/** * Passkey management procedures - list, rename, delete passkeys */ import { ORPCError } from "@orpc/server"; import { getUserPasskeys } from "../../utils/webauthn.js"; import { authMiddleware, os } from "../base.js"; /** * List passkeys handler * - Requires authentication * - Returns all passkeys for the current user */ export const listPasskeys = os.me.passkeys.list .use(authMiddleware) .handler(async ({ context }) => { const passkeys = await getUserPasskeys(context.db, context.user.id); return passkeys.map((p) => ({ id: p.id, name: p.name, createdAt: p.createdAt, lastUsedAt: p.lastUsedAt, })); }); /** * Rename passkey handler * - Requires authentication * - Updates passkey name * @throws NOT_FOUND if passkey doesn't exist */ export const renamePasskey = os.me.passkeys.rename .use(authMiddleware) .handler(async ({ input, context }) => { const { passkeyId, name } = input; const result = await context.db .updateTable("passkeys") .set({ name }) .where("id", "=", passkeyId.toString()) .where("user_id", "=", context.user.id) .executeTakeFirst(); if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Passkey not found" }); } return { success: true }; }); /** * Delete passkey handler * - Requires authentication * - Prevents deleting last passkey if user has no password * - Uses transaction to prevent race conditions * @throws NOT_FOUND if passkey doesn't exist * @throws BAD_REQUEST if trying to delete last passkey without password */ export const deletePasskey = os.me.passkeys.delete .use(authMiddleware) .handler(async ({ input, context }) => { const { passkeyId } = input; // Use transaction to prevent race condition when checking last passkey await context.db.transaction().execute(async (trx) => { // Check if this is the last passkey and user has no password const user = await trx .selectFrom("users") .select(["password_hash"]) .where("id", "=", context.user.id) .executeTakeFirstOrThrow(); const passkeyCount = await trx .selectFrom("passkeys") .select(trx.fn.countAll().as("count")) .where("user_id", "=", context.user.id) .executeTakeFirst(); if (!user.password_hash && Number(passkeyCount?.count ?? 0) <= 1) { throw new ORPCError("BAD_REQUEST", { message: "Cannot delete the last passkey when you have no password set", }); } const result = await trx .deleteFrom("passkeys") .where("id", "=", passkeyId.toString()) .where("user_id", "=", context.user.id) .executeTakeFirst(); if (!result.numDeletedRows || result.numDeletedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Passkey not found" }); } }); return { success: true }; });