import { createHash, randomBytes } from 'node:crypto'; import { db } from '../db/index.js'; const digest = (value: string) => createHash('sha256').update(value).digest('hex'); /** Private controller state, reused only by the same immutable native instance. */ export function bankCredential(instanceId: string): string { return db() .transaction(() => { const instance = db() .prepare('SELECT server FROM native_instances WHERE id=? AND retired_at IS NULL') .get(instanceId) as { server: string } | undefined; if (!instance) throw new Error('Bank credentials require an active native instance.'); const saved = db() .prepare('SELECT token FROM bank_credentials WHERE instance_id=?') .get(instanceId) as { token: string } | undefined; if (saved) return saved.token; const token = 'ec_bank_' + randomBytes(32).toString('base64url'); db() .prepare( 'INSERT INTO bank_credentials(instance_id,server,token,token_hash,created_at) VALUES (?,?,?,?,?)' ) .run(instanceId, instance.server, token, digest(token), new Date().toISOString()); return token; }) .immediate(); } /** Retiring an instance immediately revokes its game credential, including reads. */ export function authenticateBank( request: Request ): { ok: true; server: string; instanceId: string } | { ok: false; status: 401; code: string } { const token = request.headers .get('authorization') ?.match(/^Bearer (ec_bank_[A-Za-z0-9_-]{32,128})$/i)?.[1]; const matched = token ? (db() .prepare( `SELECT c.server, c.instance_id AS instanceId FROM bank_credentials c JOIN native_instances n ON n.id=c.instance_id AND n.server=c.server WHERE c.token_hash=? AND n.retired_at IS NULL` ) .get(digest(token)) as { server: string; instanceId: string } | undefined) : undefined; return matched ? { ok: true, ...matched } : { ok: false, status: 401, code: 'unauthorized' }; }