{"version":3,"file":"use-passkey.cjs","names":[],"sources":["../../src/auth/use-passkey.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the hook wraps both ceremonies with the status\n * machine the UI needs (`prompting` and `verifying` are separate because they need\n * different copy) and the capability probes that decide whether to offer the button\n * at all.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"../hooks/use-stable-callback\";\nimport {\n    classifyPasskeyError,\n    createPasskeyClient,\n    type PasskeyAuthenticationJSON,\n    type PasskeyClient,\n    type PasskeyCreationOptionsJSON,\n    type PasskeyError,\n    type PasskeyRegistrationJSON,\n    type PasskeyRequestOptionsJSON,\n} from \"./passkey\";\n\n/**\n * Where a passkey ceremony is.\n *\n * `\"prompting\"` and `\"verifying\"` are split because they need different copy:\n * while prompting, the browser sheet is up and the user must touch a sensor;\n * while verifying, your backend is checking the signature and the user should\n * just wait.\n */\nexport type PasskeyStatus = \"idle\" | \"prompting\" | \"verifying\" | \"success\" | \"error\";\n\n/** Capability probes shared by both passkey hooks. */\nexport interface PasskeyCapabilities {\n    /** WebAuthn exists in this browser. Known synchronously. */\n    supported: boolean;\n    /**\n     * This device has a built-in authenticator. `null` until the async probe\n     * settles — render nothing passkey-related while it is `null`.\n     */\n    platformAvailable: boolean | null;\n    /** Autofill-driven sign-in is available. `null` until the probe settles. */\n    conditionalAvailable: boolean | null;\n}\n\nlet sharedClient: PasskeyClient | undefined;\n\n/**\n * Lazily built default client, shared by every hook that was not given one.\n *\n * Lazy rather than a module-level constant so that importing the hooks never runs\n * a call the bundler cannot prove side-effect-free — that alone would pin the\n * module into every consumer's bundle.\n */\nfunction defaultClient(): PasskeyClient {\n    sharedClient ??= createPasskeyClient();\n    return sharedClient;\n}\n\n/**\n * Probe what this device can do, once per mount.\n *\n * @param client - Client whose probes to call.\n * @returns The three capability answers, two of them async.\n */\nexport function usePasskeyCapabilities(client?: PasskeyClient): PasskeyCapabilities {\n    const resolved = useMemo(() => client ?? defaultClient(), [client]);\n    const [supported] = useState<boolean>(() => resolved.isSupported());\n    const [platformAvailable, setPlatformAvailable] = useState<boolean | null>(null);\n    const [conditionalAvailable, setConditionalAvailable] = useState<boolean | null>(null);\n\n    useEffect(() => {\n        let cancelled = false;\n        void resolved.isPlatformAuthenticatorAvailable().then((value) => {\n            if (!cancelled) setPlatformAvailable(value);\n        });\n        void resolved.isConditionalMediationAvailable().then((value) => {\n            if (!cancelled) setConditionalAvailable(value);\n        });\n        return () => {\n            cancelled = true;\n        };\n    }, [resolved]);\n\n    return { supported, platformAvailable, conditionalAvailable };\n}\n\n/** Options for {@link usePasskeyRegistration}. */\nexport interface UsePasskeyRegistrationOptions<TResult> {\n    /** Fetch server options — your `POST /webauthn/register/begin`. */\n    getOptions: () => Promise<PasskeyCreationOptionsJSON>;\n    /** Send the credential to be verified — your `POST /webauthn/register/finish`. */\n    verify: (credential: PasskeyRegistrationJSON) => Promise<TResult>;\n    /** Client to use. Defaults to a shared `createPasskeyClient()`. */\n    client?: PasskeyClient;\n    /** Called with whatever `verify` resolved to. */\n    onSuccess?: (result: TResult) => void;\n    /** Called with the classified failure. */\n    onError?: (error: PasskeyError) => void;\n}\n\n/** Value returned by {@link usePasskeyRegistration}. */\nexport interface UsePasskeyRegistrationResult<TResult> extends PasskeyCapabilities {\n    /** Run the ceremony. Resolves `null` on failure — read `error` for the reason. */\n    register: () => Promise<TResult | null>;\n    /** Current phase. */\n    status: PasskeyStatus;\n    /** Last failure, cleared when a new attempt starts. */\n    error: PasskeyError | null;\n    /** Whatever `verify` resolved to, on success. */\n    data: TResult | null;\n    /** Close the browser sheet from your own UI. */\n    cancel: () => void;\n    /** Back to `\"idle\"`, clearing `error` and `data`. */\n    reset: () => void;\n}\n\n/**\n * Register a passkey: fetch options, run `navigator.credentials.create`, hand the\n * credential to your backend.\n *\n * `register()` **resolves** rather than rejects on failure, because every failure\n * here is already state the UI has to render (`error.kind`, `status`). Forcing a\n * `try/catch` around a button handler that also has to set state would only\n * duplicate what the hook holds.\n *\n * @param options - Transport callbacks plus optional client and listeners.\n * @returns Ceremony state, the `register` action, and the capability probes.\n *\n * @example\n * const passkey = usePasskeyRegistration({\n *     getOptions: () => api.post(\"/webauthn/register/begin\"),\n *     verify: (credential) => api.post(\"/webauthn/register/finish\", { body: credential }),\n * });\n *\n * if (passkey.platformAvailable === false) return null;\n * return (\n *     <button onClick={() => void passkey.register()} disabled={passkey.status === \"prompting\"}>\n *         Criar passkey\n *     </button>\n * );\n */\nexport function usePasskeyRegistration<TResult = unknown>(\n    options: UsePasskeyRegistrationOptions<TResult>,\n): UsePasskeyRegistrationResult<TResult> {\n    const client = useMemo(() => options.client ?? defaultClient(), [options.client]);\n    const capabilities = usePasskeyCapabilities(client);\n    const [status, setStatus] = useState<PasskeyStatus>(\"idle\");\n    const [error, setError] = useState<PasskeyError | null>(null);\n    const [data, setData] = useState<TResult | null>(null);\n    const controller = useRef<AbortController | null>(null);\n\n    const getOptions = useStableCallback(options.getOptions);\n    const verify = useStableCallback(options.verify);\n    const onSuccess = useStableCallback((result: TResult) => options.onSuccess?.(result));\n    const onError = useStableCallback((failure: PasskeyError) => options.onError?.(failure));\n\n    const register = useCallback(async (): Promise<TResult | null> => {\n        setStatus(\"prompting\");\n        setError(null);\n        const abort = new AbortController();\n        controller.current = abort;\n        try {\n            const serverOptions = await getOptions();\n            const credential = await client.register(serverOptions, { signal: abort.signal });\n            setStatus(\"verifying\");\n            const result = await verify(credential);\n            setData(result);\n            setStatus(\"success\");\n            onSuccess(result);\n            return result;\n        } catch (thrown) {\n            const failure = classifyPasskeyError(thrown, \"register\");\n            setError(failure);\n            setStatus(\"error\");\n            onError(failure);\n            return null;\n        } finally {\n            controller.current = null;\n        }\n    }, [client, getOptions, verify, onSuccess, onError]);\n\n    const cancel = useCallback((): void => {\n        controller.current?.abort();\n    }, []);\n\n    const reset = useCallback((): void => {\n        setStatus(\"idle\");\n        setError(null);\n        setData(null);\n    }, []);\n\n    return { ...capabilities, register, status, error, data, cancel, reset };\n}\n\n/** Options for {@link usePasskeySignIn}. */\nexport interface UsePasskeySignInOptions<TResult> {\n    /** Fetch server options — your `POST /webauthn/signin/begin`. */\n    getOptions: () => Promise<PasskeyRequestOptionsJSON>;\n    /** Send the assertion to be verified — your `POST /webauthn/signin/finish`. */\n    verify: (assertion: PasskeyAuthenticationJSON) => Promise<TResult>;\n    /** Client to use. Defaults to a shared `createPasskeyClient()`. */\n    client?: PasskeyClient;\n    /**\n     * Arm the autofill flow on mount (`mediation: \"conditional\"`).\n     *\n     * The request stays pending, invisibly, until the user picks a passkey from\n     * the browser's autofill list — so the field it should appear on needs\n     * `autocomplete=\"username webauthn\"`. Only one conditional request may be live\n     * per page, and it is aborted on unmount.\n     */\n    conditional?: boolean;\n    /** Called with whatever `verify` resolved to. */\n    onSuccess?: (result: TResult) => void;\n    /** Called with the classified failure. */\n    onError?: (error: PasskeyError) => void;\n}\n\n/** Value returned by {@link usePasskeySignIn}. */\nexport interface UsePasskeySignInResult<TResult> extends PasskeyCapabilities {\n    /** Run the modal ceremony. Resolves `null` on failure. */\n    signIn: () => Promise<TResult | null>;\n    /** Current phase. */\n    status: PasskeyStatus;\n    /** Last failure, cleared when a new attempt starts. */\n    error: PasskeyError | null;\n    /** Whatever `verify` resolved to, on success. */\n    data: TResult | null;\n    /** True while an armed conditional (autofill) request is waiting. */\n    conditionalPending: boolean;\n    /** Close the browser sheet from your own UI. */\n    cancel: () => void;\n    /** Back to `\"idle\"`, clearing `error` and `data`. */\n    reset: () => void;\n}\n\n/**\n * Sign in with a passkey, with or without the autofill flow.\n *\n * Two ways in, and a good login page wires both. `signIn()` is the explicit\n * button. `conditional: true` arms the pleasant one: the browser lists the user's\n * passkeys inside the username field itself, so signing in is one tap and no\n * password was ever typed. The field must carry\n * `autocomplete=\"username webauthn\"` or the list never appears — the hook cannot\n * do that part for you.\n *\n * A conditional request that is aborted (unmount, or the user typing a password\n * instead) is **not** surfaced as an error: nothing failed, the user chose\n * another door.\n *\n * @param options - Transport callbacks, `conditional`, optional client and listeners.\n * @returns Ceremony state, the `signIn` action, and the capability probes.\n *\n * @example\n * const passkey = usePasskeySignIn({\n *     conditional: true,\n *     getOptions: () => api.post(\"/webauthn/signin/begin\"),\n *     verify: (assertion) => api.post(\"/webauthn/signin/finish\", { body: assertion }),\n *     onSuccess: (session) => auth.setToken(session.access_token),\n * });\n *\n * <input name=\"email\" autoComplete=\"username webauthn\" />\n * <button onClick={() => void passkey.signIn()}>Entrar com passkey</button>\n */\nexport function usePasskeySignIn<TResult = unknown>(\n    options: UsePasskeySignInOptions<TResult>,\n): UsePasskeySignInResult<TResult> {\n    const client = useMemo(() => options.client ?? defaultClient(), [options.client]);\n    const capabilities = usePasskeyCapabilities(client);\n    const [status, setStatus] = useState<PasskeyStatus>(\"idle\");\n    const [error, setError] = useState<PasskeyError | null>(null);\n    const [data, setData] = useState<TResult | null>(null);\n    const [conditionalPending, setConditionalPending] = useState(false);\n    const controller = useRef<AbortController | null>(null);\n    const conditional = options.conditional ?? false;\n\n    const getOptions = useStableCallback(options.getOptions);\n    const verify = useStableCallback(options.verify);\n    const onSuccess = useStableCallback((result: TResult) => options.onSuccess?.(result));\n    const onError = useStableCallback((failure: PasskeyError) => options.onError?.(failure));\n\n    const run = useCallback(\n        async (\n            mediation: \"optional\" | \"conditional\",\n            abort: AbortController = new AbortController(),\n        ): Promise<TResult | null> => {\n            controller.current = abort;\n            if (mediation === \"optional\") {\n                setStatus(\"prompting\");\n                setError(null);\n            }\n            try {\n                const serverOptions = await getOptions();\n                const assertion = await client.authenticate(serverOptions, {\n                    mediation,\n                    signal: abort.signal,\n                });\n                setStatus(\"verifying\");\n                const result = await verify(assertion);\n                setData(result);\n                setStatus(\"success\");\n                onSuccess(result);\n                return result;\n            } catch (thrown) {\n                const failure = classifyPasskeyError(thrown, \"authenticate\");\n                if (failure.kind === \"aborted\" && mediation === \"conditional\") return null;\n                setError(failure);\n                setStatus(\"error\");\n                onError(failure);\n                return null;\n            } finally {\n                controller.current = null;\n            }\n        },\n        [client, getOptions, verify, onSuccess, onError],\n    );\n\n    const signIn = useCallback(() => run(\"optional\"), [run]);\n\n    useEffect(() => {\n        if (!conditional) return;\n        let cancelled = false;\n        let abort: AbortController | null = null;\n\n        async function arm(): Promise<void> {\n            if (!(await client.isConditionalMediationAvailable())) return;\n            if (cancelled) return;\n            setConditionalPending(true);\n            abort = new AbortController();\n            await run(\"conditional\", abort);\n            if (!cancelled) setConditionalPending(false);\n        }\n\n        void arm();\n\n        return () => {\n            cancelled = true;\n            abort?.abort();\n        };\n    }, [conditional, client, run]);\n\n    const cancel = useCallback((): void => {\n        controller.current?.abort();\n    }, []);\n\n    const reset = useCallback((): void => {\n        setStatus(\"idle\");\n        setError(null);\n        setData(null);\n    }, []);\n\n    return {\n        ...capabilities,\n        signIn,\n        status,\n        error,\n        data,\n        conditionalPending,\n        cancel,\n        reset,\n    };\n}\n"],"mappings":"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"}