cm_bot_v2/web/lib/auth-store.ts
yiekheng f4d5f97c42 fix(web-auth): import WebAuthn JSON types from @simplewebauthn/types
In @simplewebauthn/server v11 the JSON response and transport types are
no longer re-exported from the server package — they live in the sibling
@simplewebauthn/types package. Adds the dep and switches the imports.
2026-05-03 09:45:36 +08:00

100 lines
2.7 KiB
TypeScript

import "server-only";
import { promises as fs } from "node:fs";
import path from "node:path";
import type { AuthenticatorTransportFuture } from "@simplewebauthn/types";
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<string, PasskeyRecord[]>;
let writeLock: Promise<void> = Promise.resolve();
async function readAll(): Promise<StoreShape> {
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<void> {
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<T>(fn: () => Promise<T>): Promise<T> {
const next = writeLock.then(fn, fn);
writeLock = next.then(
() => undefined,
() => undefined,
);
return next;
}
export async function readPasskeys(username: string): Promise<PasskeyRecord[]> {
const all = await readAll();
return all[username] ?? [];
}
export async function appendPasskey(username: string, rec: PasskeyRecord): Promise<void> {
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<boolean> {
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<void> {
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);
});
}