fix sanctions and admin role escalation
This commit is contained in:
@@ -13,6 +13,10 @@
|
||||
"./auth/gameToken": {
|
||||
"types": "./dist/auth/gameToken.d.ts",
|
||||
"default": "./dist/auth/gameToken.js"
|
||||
},
|
||||
"./auth/sanctions": {
|
||||
"types": "./dist/auth/sanctions.d.ts",
|
||||
"default": "./dist/auth/sanctions.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { UserSanctions, UserServerRestriction } from './gameToken.js';
|
||||
|
||||
export type SanctionFeature = 'login' | 'game' | 'messages';
|
||||
|
||||
const FEATURE_ALIASES: Record<SanctionFeature, ReadonlySet<string>> = {
|
||||
login: new Set(['login']),
|
||||
game: new Set(['*', 'game', 'gameplay']),
|
||||
messages: new Set(['*', 'message', 'messages']),
|
||||
};
|
||||
|
||||
export const isFutureSanctionDate = (value: string | undefined, now = new Date()): boolean => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const parsed = Date.parse(value);
|
||||
return Number.isFinite(parsed) && parsed > now.getTime();
|
||||
};
|
||||
|
||||
export const isActiveServerRestriction = (
|
||||
restriction: UserServerRestriction | undefined,
|
||||
now = new Date()
|
||||
): restriction is UserServerRestriction => {
|
||||
if (!restriction) {
|
||||
return false;
|
||||
}
|
||||
return restriction.until === undefined || isFutureSanctionDate(restriction.until, now);
|
||||
};
|
||||
|
||||
export const isProfileFeatureBlocked = (
|
||||
sanctions: UserSanctions,
|
||||
profileNames: readonly string[],
|
||||
feature: SanctionFeature,
|
||||
now = new Date()
|
||||
): boolean => {
|
||||
const aliases = FEATURE_ALIASES[feature];
|
||||
for (const profileName of new Set(profileNames)) {
|
||||
const restriction = sanctions.serverRestrictions?.[profileName];
|
||||
if (!isActiveServerRestriction(restriction, now)) {
|
||||
continue;
|
||||
}
|
||||
if (restriction.blockedFeatures?.some((blockedFeature) => aliases.has(blockedFeature.trim().toLowerCase()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isLoginBanned = (sanctions: UserSanctions, now = new Date()): boolean =>
|
||||
isFutureSanctionDate(sanctions.bannedUntil, now);
|
||||
|
||||
export const isGameAccessBlocked = (
|
||||
sanctions: UserSanctions,
|
||||
profileNames: readonly string[],
|
||||
now = new Date()
|
||||
): boolean =>
|
||||
isLoginBanned(sanctions, now) ||
|
||||
isFutureSanctionDate(sanctions.suspendedUntil, now) ||
|
||||
isProfileFeatureBlocked(sanctions, profileNames, 'login', now) ||
|
||||
isProfileFeatureBlocked(sanctions, profileNames, 'game', now);
|
||||
|
||||
export const isMessageAccessBlocked = (
|
||||
sanctions: UserSanctions,
|
||||
profileNames: readonly string[],
|
||||
now = new Date()
|
||||
): boolean =>
|
||||
isGameAccessBlocked(sanctions, profileNames, now) ||
|
||||
isFutureSanctionDate(sanctions.mutedUntil, now) ||
|
||||
isProfileFeatureBlocked(sanctions, profileNames, 'messages', now);
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
isActiveServerRestriction,
|
||||
isGameAccessBlocked,
|
||||
isLoginBanned,
|
||||
isMessageAccessBlocked,
|
||||
isProfileFeatureBlocked,
|
||||
} from '../src/auth/sanctions.js';
|
||||
|
||||
const NOW = new Date('2026-07-26T00:00:00.000Z');
|
||||
|
||||
describe('sanctions', () => {
|
||||
it('treats missing restriction expiry as indefinite and ignores expired restrictions', () => {
|
||||
expect(isActiveServerRestriction({ blockedFeatures: ['game'] }, NOW)).toBe(true);
|
||||
expect(
|
||||
isActiveServerRestriction(
|
||||
{
|
||||
blockedFeatures: ['game'],
|
||||
until: '2026-07-25T23:59:59.999Z',
|
||||
},
|
||||
NOW
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches profile instance and base-profile feature aliases', () => {
|
||||
const sanctions = {
|
||||
serverRestrictions: {
|
||||
'che:default': { blockedFeatures: ['message'] },
|
||||
che: { blockedFeatures: ['gameplay'] },
|
||||
},
|
||||
};
|
||||
|
||||
expect(isProfileFeatureBlocked(sanctions, ['che:default', 'che'], 'messages', NOW)).toBe(true);
|
||||
expect(isProfileFeatureBlocked(sanctions, ['che:default', 'che'], 'game', NOW)).toBe(true);
|
||||
expect(isProfileFeatureBlocked(sanctions, ['hwe:default', 'hwe'], 'game', NOW)).toBe(false);
|
||||
});
|
||||
|
||||
it('separates login bans, gameplay suspension, and message mute', () => {
|
||||
expect(isLoginBanned({ bannedUntil: '2099-01-01T00:00:00.000Z' }, NOW)).toBe(true);
|
||||
expect(isGameAccessBlocked({ suspendedUntil: '2099-01-01T00:00:00.000Z' }, ['che'], NOW)).toBe(true);
|
||||
expect(isGameAccessBlocked({ mutedUntil: '2099-01-01T00:00:00.000Z' }, ['che'], NOW)).toBe(false);
|
||||
expect(isMessageAccessBlocked({ mutedUntil: '2099-01-01T00:00:00.000Z' }, ['che'], NOW)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ export default defineConfig({
|
||||
entry: {
|
||||
index: 'src/index.ts',
|
||||
'auth/gameToken': 'src/auth/gameToken.ts',
|
||||
'auth/sanctions': 'src/auth/sanctions.ts',
|
||||
},
|
||||
format: 'es',
|
||||
outDir: 'dist',
|
||||
|
||||
Reference in New Issue
Block a user