import { sign as jwtSign, verify as jwtVerify } from 'hono/jwt'; import { getKey } from 'officerdb/secret-store'; // The signing key comes from the secret store, not from JWT_SECRET in .env — see docs/secret-store.md. // // Read lazily and cached: importing this module must not open the store, because plenty of files import // `sign`/`verify` for their types or for one code path they may never take. // // It is created on first use rather than required up front, which is a deliberate trade. There is no // longer a startup error telling you the secret is missing; instead, losing the store file silently mints // a new key and signs everyone out of every device. That is the same failure the old env var had — it was // regenerated by the setup script on every run that agreed to rewrite .env — except the store is a file // nobody edits by hand, so it is far less likely to happen by accident. let cachedKey: string | null = null; function signingKey(): string { if (!cachedKey) cachedKey = getKey('jwt'); return cachedKey; } function parseExpiration(expiration: string): number { const match = expiration.match(/^(\d+)([smhd])$/); if (!match) { throw new Error(`Invalid expiration format: ${expiration}`); } const value = parseInt(match[1]!, 10); const unit = match[2]; const multipliers: Record = { s: 1, m: 60, h: 60 * 60, d: 60 * 60 * 24, }; return Math.floor(Date.now() / 1000) + value * multipliers[unit!]!; } export async function sign(payload: any, expiration = '30d') { const now = Math.floor(Date.now() / 1000); const exp = parseExpiration(expiration); const jti = crypto.randomUUID(); const data = { ...payload, exp, iat: now, jti }; return jwtSign(data, signingKey()); } export async function verify(token: string): Promise { return jwtVerify(token, signingKey(), 'HS256'); }