krona-staff/src/adapters/identity.ts

72 lines
2.6 KiB
TypeScript

import { timingSafeEqual } from 'node:crypto';
import type { AppConfig } from '../config.js';
import type { IdentityPort } from '../core/ports.js';
import { parseUlid, type Ulid } from '../core/ids.js';
export class StandaloneIdentityAdapter implements IdentityPort {
constructor(private readonly config: AppConfig) {}
async resolve(token: string | undefined, correlationId: Ulid) {
const permissionsByAccess = {
employee: ['staff:attendance:read_own', 'staff:shift:read', 'staff:payslip:read_own'],
manager: [
'staff:employee:read',
'staff:employee:write',
'staff:attendance:read_own',
'staff:attendance:manage',
'staff:attendance:approve',
'staff:shift:read',
'staff:shift:confirm',
'staff:org_unit:read',
'staff:org_unit:write',
'staff:metric:read',
'staff:metric:write',
'staff:pay_rule:read',
'staff:pay_rule:write',
'staff:payroll:read',
'staff:payroll:calculate',
'staff:payroll:approve',
'staff:payout:write',
],
terminal: ['staff:attendance:clock'],
} as const;
let access = this.config.STANDALONE_ACCESS;
let actorId = this.config.STANDALONE_ACTOR_ID;
if (this.config.STANDALONE_MANAGER_TOKEN && this.config.STANDALONE_TERMINAL_TOKEN) {
if (secureTokenEquals(token, this.config.STANDALONE_MANAGER_TOKEN)) {
access = 'manager';
} else if (secureTokenEquals(token, this.config.STANDALONE_TERMINAL_TOKEN)) {
access = 'terminal';
actorId = this.config.STANDALONE_TERMINAL_ACTOR_ID ?? actorId;
} else {
throw new Error('Invalid standalone access token');
}
} else if (access === 'manager') {
throw new Error('Standalone access tokens are not configured');
}
return {
tenantId: parseUlid(this.config.STANDALONE_TENANT_ID),
actorId: parseUlid(actorId),
permissions: new Set(['staff:health:read', ...permissionsByAccess[access]]),
correlationId,
};
}
}
function secureTokenEquals(candidate: string | undefined, expected: string): boolean {
if (!candidate) return false;
const candidateBuffer = Buffer.from(candidate, 'utf8');
const expectedBuffer = Buffer.from(expected, 'utf8');
return (
candidateBuffer.length === expectedBuffer.length &&
timingSafeEqual(candidateBuffer, expectedBuffer)
);
}
export class PlatformIdentityAdapter implements IdentityPort {
async resolve(): Promise<never> {
throw new Error(
'Platform JWT adapter is intentionally pending the Core gateway/JWT integration contract',
);
}
}