import { createHash, randomBytes } from 'node:crypto'; import { db } from '../db/index.js'; import type { ModerationGame } from '../../moderation.js'; const digest = (value: string) => createHash('sha256').update(value).digest('hex'); export function moderationCredential(instanceId: string, generation: number): string { return db() .transaction(() => { if ( !Number.isSafeInteger(generation) || !db() .prepare( 'SELECT 1 FROM native_instances WHERE id=? AND generation IN (?,?) AND retired_at IS NULL' ) .get(instanceId, generation, generation - 1) ) throw new Error('Moderation credentials require an active instance generation.'); const existing = db() .prepare( 'SELECT token FROM moderation_credentials WHERE instance_id=? AND generation=?' ) .get(instanceId, generation) as { token: string } | undefined; if (existing) return existing.token; const token = 'ec_moderation_' + randomBytes(32).toString('base64url'); db() .prepare('INSERT INTO moderation_credentials VALUES(?,?,?,?,?)') .run(instanceId, generation, token, digest(token), new Date().toISOString()); return token; }) .immediate(); } export interface ModerationRuntimeIdentity { instanceId: string; generation: number; server: string; game: ModerationGame; serverCreatedAt: string; credentialHash: string; } export function authenticateModerationRuntime(request: Request): ModerationRuntimeIdentity | null { const token = request.headers .get('authorization') ?.match(/^Bearer (ec_moderation_[A-Za-z0-9_-]{43})$/)?.[1]; if (!token) return null; return ( (db() .prepare( `SELECT n.id AS instanceId,n.generation,n.server,s.game,s.created_at AS serverCreatedAt,c.token_hash AS credentialHash FROM moderation_credentials c JOIN native_instances n ON n.id=c.instance_id AND n.generation=c.generation JOIN servers s ON s.name=n.server WHERE c.token_hash=? AND n.retired_at IS NULL AND s.destroyed_at IS NULL AND s.game IN ('t5','t6')` ) .get(digest(token)) as ModerationRuntimeIdentity | undefined) ?? null ); }