Files
electric-cherry/src/lib/server/auth/bank.ts
T

47 lines
2.1 KiB
TypeScript

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' };
}