fix sanctions and admin role escalation

This commit is contained in:
2026-07-26 18:11:01 +00:00
parent 3fb75ccf03
commit ab6ed3553a
13 changed files with 591 additions and 51 deletions
+4
View File
@@ -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": {
+68
View File
@@ -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);
+46
View File
@@ -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);
});
});
+1
View File
@@ -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',