/** * Passkey REST API Server — Spec Section 3.7, 16.3 * * Proxies browser FIDO2 requests to PasskeyPlatformService (→ amiPro FIDO2 server). * * Endpoints: * POST /api/passkey/attestation/options — Get registration challenge * POST /api/passkey/attestation/result — Submit registration result * POST /api/passkey/assertion/options — Get authentication challenge * POST /api/passkey/assertion/result — Submit authentication result * POST /api/passkey/session/validate — Validate a session * POST /api/passkey/session/delete — Delete/logout a session */ import http from 'node:http'; import type { PasskeyPlatformService } from './platform-service.js'; export class PasskeyAPIServer { private server: http.Server | null = null; constructor( private readonly platformService: PasskeyPlatformService, private readonly rpId: string, private readonly origin: string, ) {} start(port: number): void { this.server = http.createServer((req, res) => { void this.handleRequest(req, res); }); this.server.listen(port, '127.0.0.1', () => { console.log(`[passkey-api] Server listening on http://127.0.0.1:${port}`); }); } stop(): void { this.server?.close(); } private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise { const url = new URL(req.url ?? '/', 'http://localhost'); // CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } if (req.method !== 'POST') { sendJson(res, 405, { error: 'Method Not Allowed' }); return; } try { const body = await parseJsonBody(req) as Record; switch (url.pathname) { case '/api/passkey/attestation/options': await this.handleAttestationOptions(res, body); break; case '/api/passkey/attestation/result': await this.handleAttestationResult(res, body); break; case '/api/passkey/assertion/options': await this.handleAssertionOptions(res, body); break; case '/api/passkey/assertion/result': await this.handleAssertionResult(res, body); break; case '/api/passkey/session/validate': await this.handleSessionValidate(res, body); break; case '/api/passkey/session/delete': await this.handleSessionDelete(res, body); break; default: sendJson(res, 404, { error: 'Not Found' }); } } catch (err) { sendJson(res, 400, { error: (err as Error).message }); } } // ─── Registration ──────────────────────────────────────────────────── private async handleAttestationOptions( res: http.ServerResponse, body: Record, ): Promise { const username = body.username as string; const displayName = (body.displayName as string) || username; if (!username) { sendJson(res, 400, { error: 'username is required' }); return; } const result = await this.platformService.getAttestationOptions(username, displayName); sendJson(res, result.success ? 200 : 400, result.success ? result.options : { error: result.error }); } private async handleAttestationResult( res: http.ServerResponse, body: Record, ): Promise { const result = await this.platformService.submitAttestationResult(body as any); sendJson(res, result.success ? 200 : 400, result); } // ─── Authentication ────────────────────────────────────────────────── private async handleAssertionOptions( res: http.ServerResponse, body: Record, ): Promise { const username = body.username as string | undefined; const result = await this.platformService.getAssertionOptions(username); sendJson(res, result.success ? 200 : 400, result.success ? result.options : { error: result.error }); } private async handleAssertionResult( res: http.ServerResponse, body: Record, ): Promise { const result = await this.platformService.submitAssertionResult(body as any); sendJson(res, result.success ? 200 : 400, result); } // ─── Session ───────────────────────────────────────────────────────── private async handleSessionValidate( res: http.ServerResponse, body: Record, ): Promise { const session = body.session as string; if (!session) { sendJson(res, 400, { error: 'session is required' }); return; } const valid = await this.platformService.validateSession(session); sendJson(res, 200, { valid }); } private async handleSessionDelete( res: http.ServerResponse, body: Record, ): Promise { const session = body.session as string; const username = body.username as string; if (!session || !username) { sendJson(res, 400, { error: 'session and username are required' }); return; } await this.platformService.deleteSession(session, username); sendJson(res, 200, { ok: true }); } } function sendJson(res: http.ServerResponse, status: number, body: unknown): void { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(body)); } function parseJsonBody(req: http.IncomingMessage): Promise { return new Promise((resolve, reject) => { let data = ''; req.on('data', (chunk: Buffer) => { data += chunk.toString(); }); req.on('end', () => { try { resolve(JSON.parse(data)); } catch { reject(new Error('Invalid JSON')); } }); req.on('error', reject); }); }