import { createHmac } from 'node:crypto'; import { getKey } from 'officerdb/secret-store'; const RELAY_TOKEN_CONTEXT = 'officer-browser-relay-v1'; // ── Its own key, not the platform's ── // // This derived tokens from the platform's session signing key, and the file said, in this very // comment, that an extraction should stop doing that: "a plugin deriving tokens from the platform's // signing key is the coupling the per-purpose split exists to remove". Done now. // // Changing the key changes every derived token, so this is normally a breaking change of the same kind as // changing the port. It is free today and only today: the relay has been off since 2026-08-13 and there // are no pairings to invalidate — checked in Postgres before the change, not assumed. // // `getKey` mints a random value for a purpose that has none yet and stores it, which is right here: this // key protects nothing but its own tokens, and a fresh install having a fresh one is correct. export function deriveRelayToken(userId: number, port: number, salt: string): string { return createHmac('sha256', getKey('browser-relay')).update(`${RELAY_TOKEN_CONTEXT}:${port}:${userId}:${salt}`).digest('hex'); } const tokenToUser = new Map(); export function registerUserToken(userId: number, port: number, salt: string): string { const token = deriveRelayToken(userId, port, salt); tokenToUser.set(token, userId); return token; } export function unregisterUserToken(token: string): void { tokenToUser.delete(token); } export function restoreUserTokens(entries: Array<{ userId: number; salt: string }>, port: number): void { for (const { userId, salt } of entries) { const token = deriveRelayToken(userId, port, salt); tokenToUser.set(token, userId); } } export function resolveUserFromToken(token: string): number | null { return tokenToUser.get(token) ?? null; } export function getRelayAuthHeader(): string { return 'x-officer-relay-token'; }