import { createHash, randomBytes, randomUUID } from 'node:crypto'; import type { AuthUser } from './auth.js'; import { getDatabase } from './database.js'; export type TechnicalPermission = 'read' | 'read_write'; export type TechnicalActor = { id: string; name: string; permissions: TechnicalPermission }; function tokenHash(token: string) { return createHash('sha256').update(token).digest('hex'); } export function listTechnicalAccesses() { return getDatabase() .prepare( `SELECT id, name, permissions, created_at_utc AS createdAtUtc, last_used_at_utc AS lastUsedAtUtc, revoked_at_utc AS revokedAtUtc FROM technical_accesses ORDER BY name` ) .all() as { id: string; name: string; permissions: TechnicalPermission; createdAtUtc: string; lastUsedAtUtc: string | null; revokedAtUtc: string | null; }[]; } export function createTechnicalAccess(actor: AuthUser, nameValue: string, permissions: string) { const name = nameValue.trim(); if (!name || name.length > 120 || !['read', 'read_write'].includes(permissions)) throw new Error('INVALID_ACCESS'); const token = randomBytes(32).toString('base64url'); const id = randomUUID(); const now = new Date().toISOString(); getDatabase() .prepare( `INSERT INTO technical_accesses (id, name, token_hash, permissions, created_by_user_id, created_at_utc) VALUES (?, ?, ?, ?, ?, ?)` ) .run(id, name, tokenHash(token), permissions, actor.id, now); auditTechnical( { id: actor.id, name: actor.displayName, permissions: 'read_write' }, 'technical_access.create', 'technical_access', id, 'user' ); return { id, token }; } export function updateTechnicalAccess( actor: AuthUser, id: string, nameValue: string, permissions: string ) { const name = nameValue.trim(); if (!name || name.length > 120 || !['read', 'read_write'].includes(permissions)) throw new Error('INVALID_ACCESS'); const result = getDatabase() .prepare( `UPDATE technical_accesses SET name = ?, permissions = ? WHERE id = ? AND revoked_at_utc IS NULL` ) .run(name, permissions, id); if (!result.changes) throw new Error('NOT_FOUND'); auditTechnical( { id: actor.id, name: actor.displayName, permissions: 'read_write' }, 'technical_access.update', 'technical_access', id, 'user' ); } export function revokeTechnicalAccess(actor: AuthUser, id: string) { const result = getDatabase() .prepare( 'UPDATE technical_accesses SET revoked_at_utc = ? WHERE id = ? AND revoked_at_utc IS NULL' ) .run(new Date().toISOString(), id); if (!result.changes) throw new Error('NOT_FOUND'); auditTechnical( { id: actor.id, name: actor.displayName, permissions: 'read_write' }, 'technical_access.revoke', 'technical_access', id, 'user' ); } export function authenticateTechnical(request: Request, write = false): TechnicalActor { const authorization = request.headers.get('authorization') ?? ''; const token = authorization.startsWith('Bearer ') ? authorization.slice(7).trim() : ''; if (!token) throw new Error('API_UNAUTHORIZED'); const row = getDatabase() .prepare( `SELECT id, name, permissions FROM technical_accesses WHERE token_hash = ? AND revoked_at_utc IS NULL` ) .get(tokenHash(token)) as TechnicalActor | undefined; if (!row) throw new Error('API_UNAUTHORIZED'); if (write && row.permissions !== 'read_write') throw new Error('API_FORBIDDEN'); getDatabase() .prepare('UPDATE technical_accesses SET last_used_at_utc = ? WHERE id = ?') .run(new Date().toISOString(), row.id); return row; } export function auditTechnical( actor: TechnicalActor, action: string, targetType: string, targetId: string, actorType: 'technical' | 'user' = 'technical' ) { getDatabase() .prepare( `INSERT INTO audit_events (id, actor_type, actor_id, action, target_type, target_id, created_at_utc) VALUES (?, ?, ?, ?, ?, ?, ?)` ) .run( randomUUID(), actorType, actor.id, action.slice(0, 120), targetType.slice(0, 80), targetId.slice(0, 200), new Date().toISOString() ); } export function idempotentResponse( actor: TechnicalActor, request: Request, body: unknown, operation: () => { status: number; body: unknown } ) { const key = request.headers.get('idempotency-key')?.trim() ?? ''; if (!key || key.length > 200) throw new Error('API_IDEMPOTENCY_REQUIRED'); const hash = createHash('sha256') .update(`${request.method} ${new URL(request.url).pathname}\n${JSON.stringify(body)}`) .digest('hex'); const database = getDatabase(); return database.transaction(() => { const existing = database .prepare( 'SELECT request_hash, status_code, response_json FROM api_idempotency WHERE access_id = ? AND request_key = ?' ) .get(actor.id, key) as { request_hash: string; status_code: number; response_json: string } | undefined; if (existing) { if (existing.request_hash !== hash) throw new Error('API_IDEMPOTENCY_CONFLICT'); return { status: existing.status_code, body: JSON.parse(existing.response_json), duplicate: true }; } const result = operation(); database .prepare( `INSERT INTO api_idempotency (access_id, request_key, request_hash, status_code, response_json, created_at_utc) VALUES (?, ?, ?, ?, ?, ?)` ) .run( actor.id, key, hash, result.status, JSON.stringify(result.body), new Date().toISOString() ); return { ...result, duplicate: false }; })(); }