First running version

This commit is contained in:
qingjie.du
2026-03-30 17:39:13 +09:00
parent 5ffea3d849
commit bce2a5672c
67 changed files with 16503 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
/**
* Passkey REST API Endpoints — Spec Section 3.7, 16.3
*
* Provides REST endpoints for:
* - POST /api/passkey/register/begin — Start registration
* - POST /api/passkey/register/complete — Finish registration
* - POST /api/passkey/authenticate/begin — Start authentication
* - POST /api/passkey/authenticate/complete — Finish authentication
*/
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<void> {
const url = new URL(req.url ?? '/', 'http://localhost');
// CORS headers
res.setHeader('Access-Control-Allow-Origin', this.origin);
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, 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<string, unknown>;
if (url.pathname === '/api/passkey/register/begin') {
await this.handleRegisterBegin(res, body);
} else if (url.pathname === '/api/passkey/register/complete') {
await this.handleRegisterComplete(res, body);
} else if (url.pathname === '/api/passkey/authenticate/begin') {
await this.handleAuthenticateBegin(res, body);
} else if (url.pathname === '/api/passkey/authenticate/complete') {
await this.handleAuthenticateComplete(res, body);
} else {
sendJson(res, 404, { error: 'Not Found' });
}
} catch (err) {
sendJson(res, 400, { error: (err as Error).message });
}
}
private async handleRegisterBegin(
res: http.ServerResponse,
body: Record<string, unknown>,
): Promise<void> {
const userId = body.userId as string;
const displayName = body.displayName as string || userId;
if (!userId) {
sendJson(res, 400, { error: 'userId is required' });
return;
}
const result = await this.platformService.startRegistration(userId, displayName);
sendJson(res, result.success ? 200 : 400, result);
}
private async handleRegisterComplete(
res: http.ServerResponse,
body: Record<string, unknown>,
): Promise<void> {
const userId = body.userId as string;
const challengeB64 = body.challenge as string;
const attestationResponse = body.attestationResponse as Record<string, unknown>;
if (!userId || !challengeB64 || !attestationResponse) {
sendJson(res, 400, { error: 'Missing required fields' });
return;
}
const result = await this.platformService.completeRegistration(
userId,
challengeB64,
attestationResponse as any,
this.rpId,
this.origin,
);
sendJson(res, result.success ? 200 : 400, result);
}
private async handleAuthenticateBegin(
res: http.ServerResponse,
body: Record<string, unknown>,
): Promise<void> {
const userId = body.userId as string | undefined;
const result = await this.platformService.startAuthentication(userId);
sendJson(res, result.success ? 200 : 400, result);
}
private async handleAuthenticateComplete(
res: http.ServerResponse,
body: Record<string, unknown>,
): Promise<void> {
const credentialId = body.credentialId as string;
const challengeB64 = body.challenge as string;
const assertionResponse = body.assertionResponse as Record<string, unknown>;
if (!credentialId || !challengeB64 || !assertionResponse) {
sendJson(res, 400, { error: 'Missing required fields' });
return;
}
const result = await this.platformService.completeAuthentication(
credentialId,
challengeB64,
assertionResponse as any,
this.rpId,
this.origin,
);
sendJson(res, result.success ? 200 : 400, result);
}
}
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<unknown> {
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);
});
}