/** * Passkey data helpers for converting between database and WebAuthn formats */ import type { AuthenticatorTransportFuture } from "@simplewebauthn/types"; /** * Convert a base64url string to a Uint8Array for BYTEA storage */ export const base64urlToUint8Array = (base64url: string): Uint8Array => { return Uint8Array.from(Buffer.from(base64url, "base64url")); }; /** * Convert a Uint8Array (from BYTEA) to a base64url string */ export const uint8ArrayToBase64url = (uint8Array: Uint8Array): string => { return Buffer.from(uint8Array).toString("base64url"); }; /** * Parsed passkey data for use with simplewebauthn */ export interface ParsedPasskey { id: number; credentialId: string; publicKey: Uint8Array; counter: number; transports: AuthenticatorTransportFuture[] | null; deviceType: "singleDevice" | "multiDevice"; backupEligible: boolean; backupStatus: boolean; rpid: string; name: string; lastUsedAt: Date | null; createdAt: Date; } /** * Raw passkey row from database */ export interface PasskeyRow { id: string | number; // Int8 from DB comes as string user_id: number; credential_id: Uint8Array; public_key: Uint8Array; webauthn_user_id: string; counter: string | number | bigint; device_type: "singleDevice" | "multiDevice"; backup_eligible: boolean; backup_status: boolean; transports: unknown; rpid: string; name: string; last_used_at: Date | null; created_at: Date; } /** * Parse a passkey row from the database into a usable format */ export const parsePasskeyRow = (row: PasskeyRow): ParsedPasskey => { // Create a new Uint8Array to ensure proper ArrayBuffer type const publicKeyBytes = new Uint8Array(row.public_key); return { id: Number(row.id), // Convert Int8 (string) to number credentialId: uint8ArrayToBase64url(row.credential_id), publicKey: publicKeyBytes, counter: Number(row.counter), transports: row.transports as AuthenticatorTransportFuture[] | null, deviceType: row.device_type, backupEligible: row.backup_eligible, backupStatus: row.backup_status, rpid: row.rpid, name: row.name, lastUsedAt: row.last_used_at, createdAt: row.created_at, }; }; /** * Format a date for passkey name */ export const formatPasskeyDate = (date: Date): string => { return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "2-digit", }); };