/** * Auth middleware bridge — API tokens longue duree (`brg_*`) avec scopes. * * Tokens declares dans `BRIDGE_API_TOKENS` au format JSON : * [{"token":"brg_xxx","name":"docmost-prod","scopes":["read:personnes","write:attributions"]}] * * JSON choisi plutot qu'un mini-DSL `name:scope1,scope2;...` : parse natif, pas d'ambiguite * sur les separateurs si un nom contient virgule/point-virgule. Cf rapport Bloc 3. */ import type { MiddlewareHandler } from 'hono'; import { errors } from '../lib/errors.js'; export interface ApiTokenRecord { token: string; name: string; scopes: string[]; } export interface AuthContext { tokenName: string; scopes: ReadonlySet; } /** Hono context variable map — augmente sur l'app pour acces type-safe. */ export type AuthVariables = { auth: AuthContext; }; /** Parse `BRIDGE_API_TOKENS` (JSON). Retourne map token → record. */ export function parseTokens(raw: string | undefined): Map { const map = new Map(); if (!raw || raw.trim().length === 0) return map; let parsed: unknown; try { parsed = JSON.parse(raw); } catch { throw new Error('BRIDGE_API_TOKENS: JSON invalide'); } if (!Array.isArray(parsed)) { throw new Error('BRIDGE_API_TOKENS: doit etre un tableau JSON'); } for (const entry of parsed) { if (typeof entry !== 'object' || entry === null) { throw new Error('BRIDGE_API_TOKENS: entrees doivent etre des objets'); } const e = entry as Record; if (typeof e.token !== 'string' || typeof e.name !== 'string') { throw new Error('BRIDGE_API_TOKENS: chaque entree requiert token + name'); } if (!Array.isArray(e.scopes) || e.scopes.some((s: unknown) => typeof s !== 'string')) { throw new Error('BRIDGE_API_TOKENS: scopes doit etre un tableau de strings'); } map.set(e.token, { token: e.token, name: e.name, scopes: e.scopes as string[] }); } return map; } /** * Verifie si un set de scopes possedes couvre la demande. * `admin:*` couvre tout. Match exact sinon. */ export function hasScope(owned: ReadonlySet, required: string): boolean { if (owned.has('admin:*')) return true; return owned.has(required); } /** * Factory middleware : exige un scope precis. * Le middleware d'auth global doit avoir tourne avant pour peupler `c.var.auth`. */ export function requireScope(scope: string): MiddlewareHandler<{ Variables: AuthVariables }> { return async (c, next) => { const auth = c.get('auth'); if (!auth) { throw errors.authRequired(); } if (!hasScope(auth.scopes, scope)) { throw errors.forbidden(scope); } await next(); }; } /** * Middleware d'auth global. Lit `Authorization: Bearer brg_*`, peuple `c.var.auth`. * Pas d'enforcement de scope ici — c'est le job de `requireScope`. */ export function authMiddleware( tokens: ReadonlyMap, ): MiddlewareHandler<{ Variables: AuthVariables }> { return async (c, next) => { const header = c.req.header('Authorization'); if (!header) { throw errors.authRequired(); } const match = header.match(/^Bearer\s+(.+)$/); if (!match) { throw errors.authInvalid(); } const token = match[1].trim(); const record = tokens.get(token); if (!record) { throw errors.authInvalid(); } c.set('auth', { tokenName: record.name, scopes: new Set(record.scopes) }); await next(); }; }