/** * Passkey Storage Interface and In-Memory Implementation * Spec Section 3.7, 7.3.1 */ export interface PasskeyCredential { id: string; // base64-encoded credential ID publicKey: string; // base64-encoded public key counter: number; // signature counter (prevents cloned authenticators) transports?: string[]; // e.g., ["platform", "usb"] createdAt: string; // ISO 8601 timestamp lastUsed?: string; // ISO 8601 timestamp } export interface PasskeyUser { userId: string; displayName: string; credentials: PasskeyCredential[]; createdAt: string; // ISO 8601 timestamp } export interface IPasskeyStorage { // User operations getUser(userId: string): Promise; createUser(userId: string, displayName: string): Promise; userExists(userId: string): Promise; // Credential operations addCredential(userId: string, credential: PasskeyCredential): Promise; getCredential(credentialId: string): Promise; getCredentialsByUserId(userId: string): Promise; updateCredentialCounter(credentialId: string, counter: number): Promise; deleteCredential(userId: string, credentialId: string): Promise; } /** * In-Memory Passkey Storage Implementation * Suitable for demo/localhost mode. For production, implement persistent storage * (e.g., MongoDB, PostgreSQL, etc.) */ export class InMemoryPasskeyStorage implements IPasskeyStorage { private users = new Map(); async getUser(userId: string): Promise { return this.users.get(userId) ?? null; } async createUser(userId: string, displayName: string): Promise { if (this.users.has(userId)) { throw new Error(`User ${userId} already exists`); } const user: PasskeyUser = { userId, displayName, credentials: [], createdAt: new Date().toISOString(), }; this.users.set(userId, user); return user; } async userExists(userId: string): Promise { return this.users.has(userId); } async addCredential(userId: string, credential: PasskeyCredential): Promise { const user = this.users.get(userId); if (!user) { throw new Error(`User ${userId} not found`); } // Check for duplicate credential ID if (user.credentials.some(c => c.id === credential.id)) { throw new Error(`Credential already exists for user ${userId}`); } user.credentials.push(credential); } async getCredential(credentialId: string): Promise { for (const user of this.users.values()) { const cred = user.credentials.find(c => c.id === credentialId); if (cred) return cred; } return null; } async getCredentialsByUserId(userId: string): Promise { const user = this.users.get(userId); return user?.credentials ?? []; } async updateCredentialCounter(credentialId: string, counter: number): Promise { for (const user of this.users.values()) { const cred = user.credentials.find(c => c.id === credentialId); if (cred) { cred.counter = counter; cred.lastUsed = new Date().toISOString(); return; } } throw new Error(`Credential ${credentialId} not found`); } async deleteCredential(userId: string, credentialId: string): Promise { const user = this.users.get(userId); if (!user) { throw new Error(`User ${userId} not found`); } const idx = user.credentials.findIndex(c => c.id === credentialId); if (idx >= 0) { user.credentials.splice(idx, 1); } } }