import { startRegistration, startAuthentication, browserSupportsWebAuthn, WebAuthnAbortService, } from '@simplewebauthn/browser' export interface RegisteredKey { credentialId: string nickname: string createdAt: number transports: string[] deviceType: string | null } export const useWebAuthn = () => { const isSupported = (): boolean => { if (typeof window === 'undefined') return false try { return browserSupportsWebAuthn() } catch { return false } } const register = async (nickname: string) => { // Clear any leftover ceremony so a previously-cancelled prompt does not // leave the abort controller in a state that suppresses the next call. try { WebAuthnAbortService.cancelCeremony() } catch {} const options = await $fetch('/api/idempiere-auth/webauthn/register-options', { method: 'POST', headers: useRequestHeaders(['cookie']), }) const attestation = await startRegistration({ optionsJSON: options as any }) return await $fetch('/api/idempiere-auth/webauthn/register-verify', { method: 'POST', headers: useRequestHeaders(['cookie']), body: { response: attestation, nickname }, }) } const loginWithKey = async (opts: { mobileWorker?: boolean } = {}) => { // After a user-cancelled passkey prompt, the lib's internal AbortController // can still hold the previous signal. Without resetting it, calling // startAuthentication again silently no-ops in some browsers and the user // has to reload the page. Explicitly cancel before each attempt. try { WebAuthnAbortService.cancelCeremony() } catch {} const options = await $fetch('/api/idempiere-auth/webauthn/login-options', { method: 'POST', headers: useRequestHeaders(['cookie']), }) const assertion = await startAuthentication({ optionsJSON: options as any }) const isPwa = typeof window !== 'undefined' && (window.matchMedia('(display-mode: standalone)').matches || !!(navigator as any)['standalone']) return await $fetch('/api/idempiere-auth/webauthn/login-verify', { method: 'POST', headers: useRequestHeaders(['cookie']), body: { response: assertion, pwa: isPwa, mobileWorker: !!opts.mobileWorker }, }) } const listKeys = async (): Promise<{ keys: RegisteredKey[]; hasPasswordStored: boolean }> => { return await $fetch('/api/idempiere-auth/webauthn/list', { headers: useRequestHeaders(['cookie']), }) } const deleteKey = async (credentialId: string) => { return await $fetch('/api/idempiere-auth/webauthn/delete', { method: 'DELETE', headers: useRequestHeaders(['cookie']), body: { credentialId }, }) } return { isSupported, register, loginWithKey, listKeys, deleteKey } }