119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
/**
|
|
* 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<PasskeyUser | null>;
|
|
createUser(userId: string, displayName: string): Promise<PasskeyUser>;
|
|
userExists(userId: string): Promise<boolean>;
|
|
|
|
// Credential operations
|
|
addCredential(userId: string, credential: PasskeyCredential): Promise<void>;
|
|
getCredential(credentialId: string): Promise<PasskeyCredential | null>;
|
|
getCredentialsByUserId(userId: string): Promise<PasskeyCredential[]>;
|
|
updateCredentialCounter(credentialId: string, counter: number): Promise<void>;
|
|
deleteCredential(userId: string, credentialId: string): Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* 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<string, PasskeyUser>();
|
|
|
|
async getUser(userId: string): Promise<PasskeyUser | null> {
|
|
return this.users.get(userId) ?? null;
|
|
}
|
|
|
|
async createUser(userId: string, displayName: string): Promise<PasskeyUser> {
|
|
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<boolean> {
|
|
return this.users.has(userId);
|
|
}
|
|
|
|
async addCredential(userId: string, credential: PasskeyCredential): Promise<void> {
|
|
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<PasskeyCredential | null> {
|
|
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<PasskeyCredential[]> {
|
|
const user = this.users.get(userId);
|
|
return user?.credentials ?? [];
|
|
}
|
|
|
|
async updateCredentialCounter(credentialId: string, counter: number): Promise<void> {
|
|
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<void> {
|
|
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);
|
|
}
|
|
}
|
|
}
|