import "server-only"; import { promises as fs } from "node:fs"; import path from "node:path"; import type { AuthenticatorTransportFuture } from "@simplewebauthn/server"; const FILE_PATH = process.env.CM_AUTH_STORE_PATH ?? "/data/auth/passkeys.json"; export type PasskeyRecord = { id: string; publicKey: string; counter: number; transports: AuthenticatorTransportFuture[]; name: string; createdAt: string; }; type StoreShape = Record; let writeLock: Promise = Promise.resolve(); async function readAll(): Promise { try { const raw = await fs.readFile(FILE_PATH, "utf8"); const parsed = JSON.parse(raw); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { return parsed as StoreShape; } return {}; } catch (err) { if ((err as NodeJS.ErrnoException).code === "ENOENT") return {}; throw err; } } async function writeAtomic(data: StoreShape): Promise { const dir = path.dirname(FILE_PATH); await fs.mkdir(dir, { recursive: true }); const tmp = `${FILE_PATH}.tmp`; const handle = await fs.open(tmp, "w"); try { await handle.writeFile(JSON.stringify(data, null, 2)); await handle.sync(); } finally { await handle.close(); } await fs.rename(tmp, FILE_PATH); } function withLock(fn: () => Promise): Promise { const next = writeLock.then(fn, fn); writeLock = next.then( () => undefined, () => undefined, ); return next; } export async function readPasskeys(username: string): Promise { const all = await readAll(); return all[username] ?? []; } export async function appendPasskey(username: string, rec: PasskeyRecord): Promise { await withLock(async () => { const all = await readAll(); const list = all[username] ?? []; list.push(rec); all[username] = list; await writeAtomic(all); }); } export async function removePasskey(username: string, credentialId: string): Promise { return withLock(async () => { const all = await readAll(); const list = all[username] ?? []; const next = list.filter((p) => p.id !== credentialId); if (next.length === list.length) return false; all[username] = next; await writeAtomic(all); return true; }); } export async function bumpCounter( username: string, credentialId: string, counter: number, ): Promise { await withLock(async () => { const all = await readAll(); const list = all[username] ?? []; const idx = list.findIndex((p) => p.id === credentialId); if (idx === -1) return; list[idx] = { ...list[idx], counter }; all[username] = list; await writeAtomic(all); }); }