/** * @fileoverview API keys and HMAC session tokens for the middleware auth bridge. * @module lib/tokens */ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'; import { config } from '../config.js'; const TOKEN_PREFIX = 'zmt_'; const API_KEY_PREFIX = 'zmw_'; /** * @param {string} payload * @returns {string} */ function sign(payload) { return createHmac('sha256', config.tokenSecret).update(payload).digest('base64url'); } /** * Issue a signed middleware session token embedding tenant + Verae JWT. * * @param {object} params * @param {string} params.tenantId * @param {string} params.veraeToken * @param {string} [params.expiresAt] * @returns {string} Token string starting with `zmt_` */ export function issueSessionToken({ tenantId, veraeToken, expiresAt }) { const payload = Buffer.from( JSON.stringify({ tenantId, veraeToken, expiresAt, nonce: randomBytes(8).toString('hex'), }), ).toString('base64url'); const signature = sign(payload); return `${TOKEN_PREFIX}${payload}.${signature}`; } /** * Parse and verify a session token. * * @param {string} token * @returns {{ tenantId: string, veraeToken: string, expiresAt?: string, nonce?: string }|null} */ export function parseSessionToken(token) { if (!token?.startsWith(TOKEN_PREFIX)) return null; const raw = token.slice(TOKEN_PREFIX.length); const dot = raw.lastIndexOf('.'); if (dot === -1) return null; const payload = raw.slice(0, dot); const signature = raw.slice(dot + 1); const expected = sign(payload); const sigBuf = Buffer.from(signature); const expBuf = Buffer.from(expected); if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) { return null; } try { return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')); } catch { return null; } } /** * Generate a new public API key (`zmw_…`). * @returns {string} */ export function generateApiKey() { return `${API_KEY_PREFIX}${randomBytes(24).toString('base64url')}`; } /** * @param {unknown} value * @returns {boolean} */ export function isApiKey(value) { return typeof value === 'string' && value.startsWith(API_KEY_PREFIX); } /** * Extract bearer credential from an Authorization header. * @param {string|undefined} header * @returns {string|null} */ export function extractBearerToken(header) { if (!header) return null; const match = String(header).match(/^Bearer\s+(.+)$/i); return match?.[1] ?? null; }