diff --git a/services/stellar-wallet/package.json b/services/stellar-wallet/package.json index 46d6ce7..f664fc3 100644 --- a/services/stellar-wallet/package.json +++ b/services/stellar-wallet/package.json @@ -37,6 +37,8 @@ "typescript": "^5.8.3" }, "dependencies": { + "@simplewebauthn/server": "^13.2.1", + "@simplewebauthn/types": "^12.0.0", "@stellar/stellar-sdk": "^14.0.0-rc.3", "@types/jsonwebtoken": "^9.0.10", "@types/supertest": "^6.0.3", @@ -47,6 +49,7 @@ "jsonwebtoken": "^9.0.2", "sqlite3": "^5.1.7", "supertest": "^7.1.4", + "yn": "^3.1.1", "zod": "^4.1.1" } } diff --git a/services/stellar-wallet/src/auth/webauthn.ts b/services/stellar-wallet/src/auth/webauthn.ts index 37c7621..24ee90a 100644 --- a/services/stellar-wallet/src/auth/webauthn.ts +++ b/services/stellar-wallet/src/auth/webauthn.ts @@ -1,10 +1,274 @@ +import { + generateRegistrationOptions as generateRegOptions, + generateAuthenticationOptions as generateAuthOptions, + verifyRegistrationResponse, + verifyAuthenticationResponse, + type GenerateRegistrationOptionsOpts, + type GenerateAuthenticationOptionsOpts, + type VerifyRegistrationResponseOpts, + type VerifyAuthenticationResponseOpts, +} from '@simplewebauthn/server' +import type { RegistrationResponseJSON, AuthenticationResponseJSON } from '@simplewebauthn/types' +import { connectDB } from '../db/kyc' +import { + findWebAuthnCredentialsByUserId, + findWebAuthnCredentialByCredentialId, + updateWebAuthnCredentialCounter, + insertWebAuthnChallenge, + findAndValidateChallenge, + deleteWebAuthnChallenge, +} from '../db/webauthn' +import dotenv from 'dotenv' + +dotenv.config() + +/** + * WebAuthn Relying Party configuration + */ +interface WebAuthnConfig { + rpName: string + rpID: string + origin: string +} + +let config: WebAuthnConfig | null = null + +/** + * Initializes the WebAuthn server as a Relying Party with configuration from environment variables. + * This should be called once during application startup. + * + * @throws {Error} If required environment variables are missing + */ +export function configureWebAuthn(): WebAuthnConfig { + const rpName = process.env.RP_NAME + const rpID = process.env.RP_ID + const origin = process.env.ORIGIN + + if (!rpName || !rpID || !origin) { + throw new Error('Missing required WebAuthn environment variables: RP_NAME, RP_ID, ORIGIN') + } + + config = { + rpName, + rpID, + origin, + } + + return config +} + +/** + * Gets the current WebAuthn configuration. + * Initializes config if not already done. + */ +function getConfig(): WebAuthnConfig { + if (!config) { + return configureWebAuthn() + } + return config +} + +/** + * Generates WebAuthn registration options for a new user credential. + * This is used during the registration/enrollment phase when a user wants to + * register a new biometric authenticator (e.g., fingerprint, Face ID). + * + * @param userId - Unique identifier for the user (numeric ID from kyc table) + * @param userName - User's name or email for display + * @param userDisplayName - User's display name + * @returns Registration options to send to the client + */ +export async function generateRegistrationOptions( + userId: number, + userName: string, + userDisplayName: string, +) { + const cfg = getConfig() + const db = await connectDB() + + // Get existing credentials to exclude them from re-registration + const existingCredentials = await findWebAuthnCredentialsByUserId(db, userId) + const excludeCredentials = existingCredentials.map((cred) => ({ + id: cred.credential_id, // Already a base64 string from the database + type: 'public-key' as const, + })) + + const opts: GenerateRegistrationOptionsOpts = { + rpName: cfg.rpName, + rpID: cfg.rpID, + userName, + userDisplayName, + userID: new TextEncoder().encode(userId.toString()), + timeout: 60000, + authenticatorSelection: { + authenticatorAttachment: 'platform', + requireResidentKey: false, + residentKey: 'preferred', + userVerification: 'preferred', + }, + supportedAlgorithmIDs: [-7, -257], + excludeCredentials: excludeCredentials.length > 0 ? excludeCredentials : undefined, + attestationType: 'none', + } + + const options = await generateRegOptions(opts) + + // Store the challenge in the database + await insertWebAuthnChallenge(db, { + user_id: userId, + challenge: options.challenge, + type: 'registration', + }) + + return options +} + +/** + * Generates WebAuthn authentication options for user login. + * This is used during the authentication phase when a user wants to + * sign in using their registered biometric authenticator. + * + * @param userId - Unique identifier for the user (numeric ID from kyc table) + * @returns Authentication options to send to the client + */ +export async function generateAuthenticationOptions(userId: number) { + const cfg = getConfig() + const db = await connectDB() + + // Get user's registered credentials + const credentials = await findWebAuthnCredentialsByUserId(db, userId) + const allowCredentials = credentials.map((cred) => ({ + id: cred.credential_id, // Already a base64 string from the database + type: 'public-key' as const, + })) + + const opts: GenerateAuthenticationOptionsOpts = { + rpID: cfg.rpID, + timeout: 60000, + userVerification: 'preferred', + allowCredentials: allowCredentials.length > 0 ? allowCredentials : undefined, + } + + const options = await generateAuthOptions(opts) + + // Store the challenge in the database + await insertWebAuthnChallenge(db, { + user_id: userId, + challenge: options.challenge, + type: 'authentication', + }) + + return options +} + +/** + * Verifies a registration response from the client. + * Call this after the user completes the registration ceremony on their device. + * + * @param userId - The user identifier (numeric ID from kyc table) + * @param response - The registration response from the client + * @returns Verification result with credential data + */ +export async function verifyRegistration(userId: number, response: RegistrationResponseJSON) { + const cfg = getConfig() + const db = await connectDB() + + // Extract and validate the challenge + const clientDataJSON = JSON.parse( + Buffer.from(response.response.clientDataJSON, 'base64').toString('utf-8'), + ) + const challenge = clientDataJSON.challenge + + // Find and validate the challenge + const challengeRecord = await findAndValidateChallenge(db, userId, challenge, 'registration') + if (!challengeRecord) { + throw new Error('Invalid or expired challenge') + } + + const opts: VerifyRegistrationResponseOpts = { + response, + expectedChallenge: challenge, + expectedOrigin: cfg.origin, + expectedRPID: cfg.rpID, + requireUserVerification: true, + } + + const verification = await verifyRegistrationResponse(opts) + + // Delete the used challenge + await deleteWebAuthnChallenge(db, challengeRecord.id) + + return verification +} + /** - * WebAuthn verification module - * This is a simplified implementation for demonstration purposes. - * In a production environment, you would use a proper WebAuthn library - * like @simplewebauthn/server or similar. + * Verifies an authentication response from the client. + * Call this after the user completes the authentication ceremony on their device. + * + * @param userId - The user identifier (numeric ID from kyc table) + * @param response - The authentication response from the client + * @returns Verification result */ +export async function verifyAuthentication(userId: number, response: AuthenticationResponseJSON) { + const cfg = getConfig() + const db = await connectDB() + + // Extract and validate the challenge + const clientDataJSON = JSON.parse( + Buffer.from(response.response.clientDataJSON, 'base64').toString('utf-8'), + ) + const challenge = clientDataJSON.challenge + + // Find and validate the challenge + const challengeRecord = await findAndValidateChallenge(db, userId, challenge, 'authentication') + if (!challengeRecord) { + throw new Error('Invalid or expired challenge') + } + + // Get the credential from the database + const credential = await findWebAuthnCredentialByCredentialId(db, response.id) + if (!credential) { + throw new Error('Credential not found') + } + // Verify the credential belongs to this user + if (credential.user_id !== userId) { + throw new Error('Credential does not belong to this user') + } + + const opts: VerifyAuthenticationResponseOpts = { + response, + expectedChallenge: challenge, + expectedOrigin: cfg.origin, + expectedRPID: cfg.rpID, + credential: { + id: credential.credential_id, // Already a base64 string + publicKey: Buffer.from(credential.public_key, 'base64'), + counter: credential.counter, + }, + requireUserVerification: true, + } + + const verification = await verifyAuthenticationResponse(opts) + + // Update the counter to prevent replay attacks + if (verification.verified) { + await updateWebAuthnCredentialCounter( + db, + credential.credential_id, + verification.authenticationInfo.newCounter, + ) + } + + // Delete the used challenge + await deleteWebAuthnChallenge(db, challengeRecord.id) + + return verification +} + +/** + * Legacy interface for WebAuthn credential (for backward compatibility) + */ export interface WebAuthnCredential { id: string publicKey: string @@ -12,6 +276,9 @@ export interface WebAuthnCredential { counter: number } +/** + * Legacy interface for WebAuthn authentication response (for backward compatibility) + */ export interface WebAuthnAuthenticationResponse { id: string rawId: string @@ -25,63 +292,67 @@ export interface WebAuthnAuthenticationResponse { } /** - * Verifies a WebAuthn authentication response - * @param user_id - The user identifier + * Gets user credentials from database (backward compatible with existing login router) + * @param user_id - The user identifier (string for backward compatibility) + * @returns Promise - Array of user credentials + */ +export async function getUserCredentials(user_id: string): Promise { + const db = await connectDB() + const userId = Number.parseInt(user_id, 10) + + if (isNaN(userId)) { + return [] + } + + const credentials = await findWebAuthnCredentialsByUserId(db, userId) + + return credentials.map((cred) => ({ + id: cred.credential_id, + publicKey: cred.public_key, + user_id: cred.user_id.toString(), + counter: cred.counter, + })) +} + +/** + * Verifies a WebAuthn authentication response (backward compatible with existing login router) + * @param user_id - The user identifier (string for backward compatibility) * @param authResponse - The WebAuthn authentication response - * @param credentials - Array of stored credentials for the user + * @param credentials - Array of stored credentials for the user (optional, will fetch if not provided) * @returns Promise - true if verification succeeds */ -export const verifyWebAuthnAuthentication = async ( +export async function verifyWebAuthnAuthentication( user_id: string, authResponse: WebAuthnAuthenticationResponse, - credentials: WebAuthnCredential[], -): Promise => { + _credentials?: WebAuthnCredential[], +): Promise { try { - // Find the credential for this user - const credential = credentials.find((cred) => cred.user_id === user_id) - if (!credential) { - throw new Error('No credential found for user') - } + const userId = Number.parseInt(user_id, 10) - // In a real implementation, you would: - // 1. Parse the clientDataJSON - // 2. Verify the challenge - // 3. Parse the authenticatorData - // 4. Verify the signature using the stored public key - // 5. Check the counter to prevent replay attacks + if (isNaN(userId)) { + throw new Error('Invalid user ID') + } - // For this demo, we'll do basic validation - if (!authResponse.id || !authResponse.response.signature) { - throw new Error('Invalid authentication response') + // Convert the legacy auth response to the format expected by @simplewebauthn + const response: AuthenticationResponseJSON = { + id: authResponse.id, + rawId: authResponse.rawId, + response: { + authenticatorData: authResponse.response.authenticatorData, + clientDataJSON: authResponse.response.clientDataJSON, + signature: authResponse.response.signature, + userHandle: authResponse.response.userHandle, + }, + type: authResponse.type, + clientExtensionResults: {}, } - // Mock verification - in production, this would be real cryptographic verification - // For now, we'll accept any response with valid structure - return true + // Verify using the real implementation + const verification = await verifyAuthentication(userId, response) + + return verification.verified } catch (error) { console.error('WebAuthn verification failed:', error) return false } } - -/** - * Mock function to get user credentials from database - * In a real implementation, this would query the credentials table - * @param user_id - The user identifier - * @returns Promise - Array of user credentials - */ -export const getUserCredentials = async (user_id: string): Promise => { - // Mock implementation - in production, this would query the database - // For now, return a mock credential if user_id exists - if (user_id && user_id.length > 0) { - return [ - { - id: 'mock-credential-id', - publicKey: 'mock-public-key', - user_id, - counter: 1, - }, - ] - } - return [] -} diff --git a/services/stellar-wallet/src/config/dbs.sqlite b/services/stellar-wallet/src/config/dbs.sqlite new file mode 100644 index 0000000..efe5a6f Binary files /dev/null and b/services/stellar-wallet/src/config/dbs.sqlite differ diff --git a/services/stellar-wallet/src/db/init.ts b/services/stellar-wallet/src/db/init.ts new file mode 100644 index 0000000..eeeec67 --- /dev/null +++ b/services/stellar-wallet/src/db/init.ts @@ -0,0 +1,13 @@ +import { connectDB, initializeKycTable, initializeAccountsTable } from './kyc' +import { initializeWebAuthnCredentialsTable, initializeWebAuthnChallengesTable } from './webauthn' + +export async function initializeDatabase(): Promise { + const db = await connectDB() + + await initializeKycTable(db) + await initializeAccountsTable(db) + await initializeWebAuthnCredentialsTable(db) + await initializeWebAuthnChallengesTable(db) + + console.log('Database initialized successfully') +} diff --git a/services/stellar-wallet/src/db/kyc.ts b/services/stellar-wallet/src/db/kyc.ts index 3965991..de039d5 100644 --- a/services/stellar-wallet/src/db/kyc.ts +++ b/services/stellar-wallet/src/db/kyc.ts @@ -15,7 +15,7 @@ let dbInstance: sqlite3.Database | null = null export type KycRow = { id: number - name: string + email: string document: string status: string } @@ -69,13 +69,14 @@ export async function initializeKycTable(db?: sqlite3.Database): Promise { const sql = ` CREATE TABLE IF NOT EXISTS kyc ( id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - document TEXT NOT NULL, + email TEXT NOT NULL, + name TEXT, + document TEXT, status TEXT NOT NULL DEFAULT 'pending' ); ` await run(conn, sql) - await run(conn, 'CREATE UNIQUE INDEX IF NOT EXISTS idx_kyc_document ON kyc (document);') + await run(conn, 'CREATE UNIQUE INDEX IF NOT EXISTS idx_kyc_email ON kyc (email);') } /** diff --git a/services/stellar-wallet/src/db/webauthn.ts b/services/stellar-wallet/src/db/webauthn.ts new file mode 100644 index 0000000..323a475 --- /dev/null +++ b/services/stellar-wallet/src/db/webauthn.ts @@ -0,0 +1,175 @@ +import type sqlite3 from 'sqlite3' +import { connectDB, run, all } from './kyc' + +export type WebAuthnCredentialRow = { + id: number + user_id: number + credential_id: string + public_key: string + counter: number + transports: string + created_at: string +} + +export type WebAuthnChallengeRow = { + id: number + user_id: number + challenge: string + type: 'registration' | 'authentication' + created_at: string + expires_at: string +} + +export async function initializeWebAuthnCredentialsTable(db?: sqlite3.Database): Promise { + const conn = db ?? (await connectDB()) + + await run( + conn, + ` + CREATE TABLE IF NOT EXISTS webauthn_credentials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + credential_id TEXT NOT NULL, + public_key TEXT NOT NULL, + counter INTEGER NOT NULL DEFAULT 0, + transports TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES kyc(id) ON DELETE CASCADE + ); + `, + ) + + await run( + conn, + 'CREATE UNIQUE INDEX IF NOT EXISTS idx_webauthn_credential_id ON webauthn_credentials (credential_id);', + ) + await run( + conn, + 'CREATE INDEX IF NOT EXISTS idx_webauthn_user_id ON webauthn_credentials (user_id);', + ) +} + +export async function initializeWebAuthnChallengesTable(db?: sqlite3.Database): Promise { + const conn = db ?? (await connectDB()) + + await run( + conn, + ` + CREATE TABLE IF NOT EXISTS webauthn_challenges ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + challenge TEXT NOT NULL, + type TEXT NOT NULL CHECK(type IN ('registration', 'authentication')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + expires_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES kyc(id) ON DELETE CASCADE + ); + `, + ) + + await run( + conn, + 'CREATE INDEX IF NOT EXISTS idx_webauthn_challenges_user_id ON webauthn_challenges (user_id);', + ) + await run( + conn, + 'CREATE INDEX IF NOT EXISTS idx_webauthn_challenges_challenge ON webauthn_challenges (challenge);', + ) +} + +export async function insertWebAuthnCredential( + db: sqlite3.Database, + args: { + user_id: number + credential_id: string + public_key: string + counter: number + transports?: string[] + }, +): Promise { + const sql = ` + INSERT INTO webauthn_credentials (user_id, credential_id, public_key, counter, transports) + VALUES (?, ?, ?, ?, ?); + ` + const transports = args.transports ? JSON.stringify(args.transports) : null + await run(db, sql, [args.user_id, args.credential_id, args.public_key, args.counter, transports]) +} + +export async function findWebAuthnCredentialsByUserId( + db: sqlite3.Database, + userId: number, +): Promise { + return await all( + db, + 'SELECT * FROM webauthn_credentials WHERE user_id = ?;', + [userId], + ) +} + +export async function findWebAuthnCredentialByCredentialId( + db: sqlite3.Database, + credentialId: string, +): Promise { + const rows = await all( + db, + 'SELECT * FROM webauthn_credentials WHERE credential_id = ? LIMIT 1;', + [credentialId], + ) + return rows.length ? rows[0] : null +} + +export async function updateWebAuthnCredentialCounter( + db: sqlite3.Database, + credentialId: string, + newCounter: number, +): Promise { + await run(db, 'UPDATE webauthn_credentials SET counter = ? WHERE credential_id = ?;', [ + newCounter, + credentialId, + ]) +} + +export async function insertWebAuthnChallenge( + db: sqlite3.Database, + args: { + user_id: number + challenge: string + type: 'registration' | 'authentication' + expiresInSeconds?: number + }, +): Promise { + const expiresInSeconds = args.expiresInSeconds ?? 120 // 2 minutes default + const sql = ` + INSERT INTO webauthn_challenges (user_id, challenge, type, expires_at) + VALUES (?, ?, ?, datetime('now', '+${expiresInSeconds} seconds')); + ` + await run(db, sql, [args.user_id, args.challenge, args.type]) +} + +export async function findAndValidateChallenge( + db: sqlite3.Database, + userId: number, + challenge: string, + type: 'registration' | 'authentication', +): Promise { + const rows = await all( + db, + `SELECT * FROM webauthn_challenges + WHERE user_id = ? AND challenge = ? AND type = ? + AND datetime('now') < datetime(expires_at) + LIMIT 1;`, + [userId, challenge, type], + ) + return rows.length ? rows[0] : null +} + +export async function deleteWebAuthnChallenge( + db: sqlite3.Database, + challengeId: number, +): Promise { + await run(db, 'DELETE FROM webauthn_challenges WHERE id = ?;', [challengeId]) +} + +export async function deleteExpiredChallenges(db: sqlite3.Database): Promise { + await run(db, "DELETE FROM webauthn_challenges WHERE datetime('now') >= datetime(expires_at);") +} diff --git a/services/stellar-wallet/src/index.ts b/services/stellar-wallet/src/index.ts index 8c67d18..2e6aa1a 100644 --- a/services/stellar-wallet/src/index.ts +++ b/services/stellar-wallet/src/index.ts @@ -6,6 +6,9 @@ import { authLoginRouter } from './routes/auth-login' import { kycRouter } from './routes/kyc' import { kycVerifyRouter } from './routes/kyc-verify' import { walletRouter } from './routes/wallet' +import { webauthnRegisterRouter } from './routes/webauthn-register' +import { webauthnAuthenticateRouter } from './routes/webauthn-authenticate' +import { initializeDatabase } from './db/init' export const app = express() @@ -13,6 +16,15 @@ export const app = express() app.use(cors()) app.use(express.json()) +initializeDatabase() + .then(() => { + console.log('Database initialized successfully') + }) + .catch((err) => { + console.error('Failed to initialize database:', err) + process.exit(1) + }) + // Routes app.get('/health', (_req: Request, res: Response) => { res.status(200).json({ status: 'ok' }) @@ -22,6 +34,9 @@ app.post('/auth', authLimiter, (_req: Request, res: Response) => { res.status(200).json({}) }) +app.use('/api/webauthn', authLimiter, webauthnRegisterRouter) +app.use('/api/webauthn', authLimiter, webauthnAuthenticateRouter) + // Mount auth login routes app.use('/auth', authLoginRouter) diff --git a/services/stellar-wallet/src/routes/webauthn-authenticate.ts b/services/stellar-wallet/src/routes/webauthn-authenticate.ts new file mode 100644 index 0000000..08aa6ed --- /dev/null +++ b/services/stellar-wallet/src/routes/webauthn-authenticate.ts @@ -0,0 +1,63 @@ +import { Router, type Request, type Response } from 'express' +import { verifyAuthentication, generateAuthenticationOptions } from '../auth/webauthn' + +export const webauthnAuthenticateRouter = Router() + +/** + * POST /api/webauthn/authenticate/options + * Generates authentication options for a user to sign in with biometrics. + */ +webauthnAuthenticateRouter.post( + '/authenticate/options', + async (req: Request, res: Response): Promise => { + try { + const { user_id } = req.body + + if (!user_id) { + res.status(400).json({ error: 'Missing required field: user_id' }) + return + } + + const options = await generateAuthenticationOptions(user_id) + + res.json(options) + } catch (error) { + console.error('Error generating authentication options:', error) + res.status(500).json({ error: 'Failed to generate authentication options' }) + } + }, +) + +/** + * POST /api/webauthn/authenticate/verify + * Verifies the authentication response from the client. + */ +webauthnAuthenticateRouter.post( + 'authenticate/verify', + async (req: Request, res: Response): Promise => { + try { + const { user_id, response } = req.body + + if (!user_id || !response) { + res.status(400).json({ error: 'Missing required fields: user_id, response' }) + return + } + + const verification = await verifyAuthentication(user_id, response) + + if (!verification.verified) { + res.status(400).json({ error: 'Authentication verification failed' }) + return + } + + res.json({ + verified: true, + message: 'Authentication successful', + user_id: user_id, + }) + } catch (error) { + console.error('Error verifying authentication:', error) + res.status(500).json({ error: 'Failed to verify authentication' }) + } + }, +) diff --git a/services/stellar-wallet/src/routes/webauthn-register.ts b/services/stellar-wallet/src/routes/webauthn-register.ts new file mode 100644 index 0000000..039589c --- /dev/null +++ b/services/stellar-wallet/src/routes/webauthn-register.ts @@ -0,0 +1,81 @@ +import { Router, type Request, type Response } from 'express' +import { connectDB } from '../db/kyc' +import { insertWebAuthnCredential } from '../db/webauthn' +import { generateRegistrationOptions, verifyRegistration } from '../auth/webauthn' + +export const webauthnRegisterRouter = Router() + +/** + * POST /register/options + * Generates registration options for a user to register a new biometric credential. + */ +webauthnRegisterRouter.post( + '/register/otpions', + async (req: Request, res: Response): Promise => { + try { + const { user_id, user_name, user_display_name } = req.body + + if (!user_id || !user_name || !user_display_name) { + res.status(400).json({ + error: 'Missing required fields: user_id, user_name, user_display_name', + }) + return + } + + const options = await generateRegistrationOptions(user_id, user_name, user_display_name) + + res.json(options) + } catch (error) { + console.error('Error generating registration options:', error) + res.status(500).json({ error: 'Failed to generate registration options' }) + } + }, +) + +/** + * POST /register/verify + * Verifies the registration response from the client and stores the credential. + */ +webauthnRegisterRouter.post( + '/register/verify', + async (req: Request, res: Response): Promise => { + try { + const { user_id, response } = req.body + + if (!user_id || !response) { + res.status(400).json({ error: 'Missing required fields: user_id, response' }) + return + } + + const verification = await verifyRegistration(user_id, response) + + if (!verification.verified || !verification.registrationInfo) { + res.status(400).json({ error: 'Registration verification failed' }) + return + } + + const { credential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo + + // Store the credential in the database + const db = await connectDB() + await insertWebAuthnCredential(db, { + user_id: user_id, + credential_id: Buffer.from(credential.id).toString('base64'), + public_key: Buffer.from(credential.publicKey).toString('base64'), + counter: credential.counter, + transports: response.response.transports, + }) + + res.json({ + verified: true, + message: 'Registration successful', + credentialId: Buffer.from(credential.id).toString('base64'), + deviceType: credentialDeviceType, + backedUp: credentialBackedUp, + }) + } catch (error) { + console.error('Error verifying registration:', error) + res.status(500).json({ error: 'Failed to verify registration' }) + } + }, +)