@orangecheck/auth-client 2.33.4 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/appearance.mjs +23 -0
  2. package/dist/appearance.mjs.map +1 -0
  3. package/dist/bond.mjs +20 -0
  4. package/dist/bond.mjs.map +1 -0
  5. package/dist/chunk-4E7GJ5IB.mjs +1184 -0
  6. package/dist/chunk-4E7GJ5IB.mjs.map +1 -0
  7. package/dist/chunk-7ST4Q5KR.mjs +346 -0
  8. package/dist/chunk-7ST4Q5KR.mjs.map +1 -0
  9. package/dist/chunk-7UYPMTAV.mjs +652 -0
  10. package/dist/chunk-7UYPMTAV.mjs.map +1 -0
  11. package/dist/chunk-BYAF2P2L.mjs +253 -0
  12. package/dist/chunk-BYAF2P2L.mjs.map +1 -0
  13. package/dist/chunk-CYR7J4UY.mjs +118 -0
  14. package/dist/chunk-CYR7J4UY.mjs.map +1 -0
  15. package/dist/chunk-H2TXOCXH.mjs +23 -0
  16. package/dist/chunk-H2TXOCXH.mjs.map +1 -0
  17. package/dist/chunk-HZ4FJMF3.mjs +490 -0
  18. package/dist/chunk-HZ4FJMF3.mjs.map +1 -0
  19. package/dist/chunk-TJJNTITX.mjs +187 -0
  20. package/dist/chunk-TJJNTITX.mjs.map +1 -0
  21. package/dist/chunk-UFRU3I3D.mjs +913 -0
  22. package/dist/chunk-UFRU3I3D.mjs.map +1 -0
  23. package/dist/chunk-VRLFZAGK.mjs +37 -0
  24. package/dist/chunk-VRLFZAGK.mjs.map +1 -0
  25. package/dist/chunk-XCZVSE7M.mjs +40 -0
  26. package/dist/chunk-XCZVSE7M.mjs.map +1 -0
  27. package/dist/components.mjs +19 -0
  28. package/dist/components.mjs.map +1 -0
  29. package/dist/index.js +77 -67
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +112 -4001
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/linked-identities.mjs +41 -0
  34. package/dist/linked-identities.mjs.map +1 -0
  35. package/dist/provider.mjs +17 -0
  36. package/dist/provider.mjs.map +1 -0
  37. package/dist/return-to.mjs +18 -0
  38. package/dist/return-to.mjs.map +1 -0
  39. package/dist/signin.mjs +23 -0
  40. package/dist/signin.mjs.map +1 -0
  41. package/dist/sudo.mjs +16 -0
  42. package/dist/sudo.mjs.map +1 -0
  43. package/dist/tab-session.mjs +29 -0
  44. package/dist/tab-session.mjs.map +1 -0
  45. package/dist/types.mjs +17 -0
  46. package/dist/types.mjs.map +1 -0
  47. package/dist/webauthn.mjs +15 -0
  48. package/dist/webauthn.mjs.map +1 -0
  49. package/package.json +2 -1
  50. package/dist/index.d.mts +0 -303
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { useOcSession } from './provider';\nimport type { OcAccount } from './types';\n\nfunction shortenAddress(addr: string): string {\n if (addr.length <= 12) return addr;\n return `${addr.slice(0, 6)}…${addr.slice(-4)}`;\n}\n\nfunction shortenAddressMid(addr: string): string {\n if (addr.length <= 16) return addr;\n return `${addr.slice(0, 8)}…${addr.slice(-6)}`;\n}\n\nfunction isPrefixOf(value: string, target: string): boolean {\n return target.toLowerCase().startsWith(value.toLowerCase());\n}\n\nlet listboxIdCounter = 0;\nfunction useUniqueId(prefix: string): string {\n const [id] = React.useState(() => `${prefix}-${++listboxIdCounter}`);\n return id;\n}\n\nexport interface OcAccountChipProps {\n /**\n * URL to the user's account dashboard. Defaults to `https://ochk.io/dashboard`,\n * the canonical family target.\n */\n dashboardUrl?: string;\n /**\n * Override the sign-in URL shown to anonymous visitors. Defaults to\n * the session's `signInUrl` (which points at the auth host's\n * /signin?return_to=… page). Set this to a local path like `'/signin'`\n * when the consumer site mounts its own in-place `<OcSignIn />` —\n * skips the redirect-bounce entirely.\n */\n signInUrl?: string;\n /**\n * Label for the sign-in link shown to anonymous visitors.\n * Defaults to `sign in`.\n */\n signInLabel?: string;\n /** className for the wrapper `<div>`. */\n className?: string;\n /** className for the trigger pill button. */\n triggerClassName?: string;\n /** className for the dropdown popover. */\n popoverClassName?: string;\n /** className for menu items (dashboard link + sign-out button). */\n menuItemClassName?: string;\n}\n\n/**\n * Single header-account affordance for every ochk.io subdomain.\n *\n * anonymous → \"sign in\" link\n * signed in → pill `bc1q…7ke3 ▾` that opens a popover with:\n * • § signed in (label)\n * • full address (monospace, break-all)\n * • → dashboard\n * • → sign out\n *\n * The popover closes on outside click or Escape. Reasonable inline styles\n * cover the dark-mode default; override per-part via the className props\n * or via the `[data-oc-account-chip-*]` data attributes.\n */\nexport function OcAccountChip({\n dashboardUrl = 'https://ochk.io/dashboard',\n signInUrl: signInUrlOverride,\n signInLabel = 'sign in',\n className,\n triggerClassName,\n popoverClassName,\n menuItemClassName,\n}: OcAccountChipProps): React.ReactElement | null {\n const { status, account, signInUrl: sessionSignInUrl, signOut } = useOcSession();\n const signInUrl = signInUrlOverride ?? sessionSignInUrl;\n const [open, setOpen] = React.useState(false);\n const wrapRef = React.useRef<HTMLDivElement | null>(null);\n\n React.useEffect(() => {\n if (!open) return;\n function handleClickOutside(e: MouseEvent) {\n if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n }\n function handleEscape(e: KeyboardEvent) {\n if (e.key === 'Escape') setOpen(false);\n }\n document.addEventListener('mousedown', handleClickOutside);\n document.addEventListener('keydown', handleEscape);\n return () => {\n document.removeEventListener('mousedown', handleClickOutside);\n document.removeEventListener('keydown', handleEscape);\n };\n }, [open]);\n\n if (status === 'loading') return null;\n\n if (status !== 'authenticated' || !account) {\n return (\n <a\n href={signInUrl}\n className={triggerClassName ?? className}\n data-oc-account-chip-signin=\"\"\n >\n {signInLabel}\n </a>\n );\n }\n\n return (\n <div\n ref={wrapRef}\n className={className}\n data-oc-account-chip=\"\"\n style={{ position: 'relative', display: 'inline-block' }}\n >\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-label={`Signed in as ${account.didOc}. Open account menu.`}\n className={triggerClassName}\n data-oc-account-chip-trigger=\"\"\n style={\n triggerClassName\n ? undefined\n : {\n display: 'inline-flex',\n alignItems: 'center',\n gap: '0.4rem',\n padding: '0.25rem 0.625rem',\n borderRadius: '9999px',\n border: '1px solid var(--border, #27272a)',\n background:\n 'color-mix(in oklch, var(--muted, #27272a) 40%, transparent)',\n color: 'var(--foreground, #fafafa)',\n fontFamily: 'ui-monospace, SFMono-Regular, monospace',\n fontSize: 12,\n cursor: 'pointer',\n }\n }\n >\n <span aria-hidden=\"true\" style={{ color: '#22c55e', fontSize: 8 }}>\n ●\n </span>\n <span>{account.displayName ?? shortenAddress(account.didOc)}</span>\n <svg\n width=\"9\"\n height=\"9\"\n viewBox=\"0 0 10 10\"\n aria-hidden=\"true\"\n style={{\n transition: 'transform 120ms',\n transform: open ? 'rotate(180deg)' : 'rotate(0)',\n opacity: 0.6,\n }}\n >\n <path\n d=\"M2 4 L5 7 L8 4\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n fill=\"none\"\n strokeLinecap=\"round\"\n />\n </svg>\n </button>\n\n {open && (\n <div\n role=\"menu\"\n aria-label=\"Account menu\"\n className={popoverClassName}\n data-oc-account-chip-popover=\"\"\n style={\n popoverClassName\n ? { position: 'absolute', zIndex: 50, top: 'calc(100% + 6px)', right: 0 }\n : {\n position: 'absolute',\n zIndex: 50,\n top: 'calc(100% + 6px)',\n right: 0,\n minWidth: 240,\n background: 'var(--popover, #0a0a0a)',\n color: 'var(--popover-foreground, #fafafa)',\n border: '1px solid var(--border, #27272a)',\n boxShadow: '0 10px 32px -4px rgba(0,0,0,0.35)',\n fontFamily: 'ui-monospace, SFMono-Regular, monospace',\n }\n }\n >\n <div\n data-oc-account-chip-header=\"\"\n style={{\n padding: '0.75rem 0.75rem 0.625rem',\n borderBottom: '1px solid var(--border, #27272a)',\n }}\n >\n <div\n data-oc-account-chip-label=\"\"\n style={{\n color: 'var(--primary, #f97316)',\n fontSize: 10,\n letterSpacing: '0.18em',\n textTransform: 'uppercase',\n marginBottom: 4,\n }}\n >\n § signed in\n </div>\n <div\n data-oc-account-chip-address=\"\"\n style={{\n color: 'var(--popover-foreground, #fafafa)',\n fontSize: 11,\n wordBreak: 'break-all',\n lineHeight: 1.35,\n }}\n >\n {account.didOc}\n </div>\n {account.displayName ? (\n <div\n style={{\n color: 'var(--muted-foreground, #a1a1aa)',\n fontSize: 10,\n marginTop: 4,\n letterSpacing: '0.06em',\n }}\n >\n {account.displayName}\n </div>\n ) : null}\n </div>\n <div role=\"none\" style={{ padding: '0.25rem' }}>\n {dashboardUrl ? (\n <a\n role=\"menuitem\"\n href={dashboardUrl}\n onClick={() => setOpen(false)}\n className={menuItemClassName}\n data-oc-account-chip-item=\"\"\n style={\n menuItemClassName\n ? undefined\n : {\n display: 'flex',\n alignItems: 'center',\n gap: '0.5rem',\n padding: '0.5rem 0.625rem',\n fontSize: 12,\n color: 'var(--popover-foreground, #fafafa)',\n textDecoration: 'none',\n cursor: 'pointer',\n }\n }\n >\n <span\n aria-hidden=\"true\"\n style={{ color: 'var(--muted-foreground, #a1a1aa)' }}\n >\n →\n </span>\n <span style={{ flex: 1 }}>dashboard</span>\n <span\n style={{\n color: 'var(--muted-foreground, #a1a1aa)',\n fontSize: 10,\n }}\n >\n ↗\n </span>\n </a>\n ) : null}\n <button\n type=\"button\"\n role=\"menuitem\"\n onClick={() => {\n setOpen(false);\n void signOut();\n }}\n className={menuItemClassName}\n data-oc-account-chip-item=\"\"\n data-oc-account-chip-signout=\"\"\n style={\n menuItemClassName\n ? undefined\n : {\n display: 'flex',\n alignItems: 'center',\n gap: '0.5rem',\n width: '100%',\n padding: '0.5rem 0.625rem',\n fontSize: 12,\n color: 'var(--popover-foreground, #fafafa)',\n background: 'transparent',\n border: 0,\n fontFamily: 'inherit',\n textAlign: 'left',\n cursor: 'pointer',\n }\n }\n >\n <span\n aria-hidden=\"true\"\n style={{ color: 'var(--muted-foreground, #a1a1aa)' }}\n >\n →\n </span>\n <span style={{ flex: 1 }}>sign out</span>\n </button>\n </div>\n </div>\n )}\n </div>\n );\n}\n\nexport interface OcSignInButtonProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\n /** Label shown when no user is signed in. Defaults to `sign in`. */\n label?: string;\n /**\n * When `true`, render an `<a>` even while the session is loading, to\n * avoid layout shift. Defaults to `false` (renders nothing while loading).\n */\n eager?: boolean;\n /**\n * Override the destination URL. Defaults to the session's `signInUrl`\n * (the auth host's redirect-style /signin). Set this to a local path\n * like `'/signin'` when the consumer site has its own in-place\n * `<OcSignIn />` page — keeps the user in-tab.\n */\n signInUrl?: string;\n}\n\n/**\n * Drop-in sign-in button. Renders an anchor that points at either:\n *\n * - the consumer's local `/signin` page (when `signInUrl` prop is set\n * — recommended; works with `<OcSignIn />`), or\n * - the auth host's `/signin?return_to=…` (default fallback)\n *\n * When the user is already authenticated it renders nothing.\n */\nexport function OcSignInButton({\n label = 'sign in',\n eager = false,\n className,\n signInUrl: signInUrlOverride,\n ...rest\n}: OcSignInButtonProps): React.ReactElement | null {\n const { status, signInUrl: sessionSignInUrl } = useOcSession();\n const href = signInUrlOverride ?? sessionSignInUrl;\n if (status === 'authenticated') return null;\n if (!eager && status === 'loading') return null;\n\n return (\n <a\n {...rest}\n href={href}\n className={className}\n data-oc-sign-in-button=\"\"\n >\n {label}\n </a>\n );\n}\n\nexport interface OcAccountPillProps extends React.HTMLAttributes<HTMLDivElement> {\n /** URL to link the address to. Defaults to the auth origin's `/dashboard`. */\n dashboardUrl?: string;\n /** Override the display text. Defaults to the shortened address. */\n render?: (account: { address: string; displayName?: string | null }) => React.ReactNode;\n}\n\n/**\n * Shows the signed-in user as a short pill: `bc1q…abcd sign out`.\n *\n * Renders nothing while loading or when no user is signed in — pair with\n * `<OcSignInButton>` for the anonymous case.\n */\nexport function OcAccountPill({\n dashboardUrl,\n render,\n className,\n ...rest\n}: OcAccountPillProps): React.ReactElement | null {\n const { status, account, signOut } = useOcSession();\n\n if (status !== 'authenticated' || !account) return null;\n\n const label = render\n ? render({ address: account.didOc, displayName: account.displayName })\n : (account.displayName ?? shortenAddress(account.didOc));\n\n return (\n <div\n {...rest}\n className={className}\n data-oc-account-pill=\"\"\n style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', ...(rest.style ?? {}) }}\n >\n {dashboardUrl ? (\n <a href={dashboardUrl}>{label}</a>\n ) : (\n <span>{label}</span>\n )}\n <button\n type=\"button\"\n onClick={() => {\n void signOut();\n }}\n aria-label=\"Sign out\"\n style={{\n background: 'none',\n border: 'none',\n cursor: 'pointer',\n color: 'inherit',\n font: 'inherit',\n padding: 0,\n }}\n >\n sign out\n </button>\n </div>\n );\n}\n\nexport interface OcAddressInputProps\n extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {\n /** Controlled value. */\n value: string;\n /** Called when value changes — typed by the user OR selected from the popover. */\n onValueChange: (value: string) => void;\n /** Label shown above the suggested address in the popover. Defaults to `use your address`. */\n suggestionLabel?: string;\n /** className applied to the wrapper `<div>`. */\n wrapperClassName?: string;\n /** className applied to the suggestion popover. Style with `[data-oc-address-popover]` otherwise. */\n popoverClassName?: string;\n /** className applied to the suggestion button. Style with `[data-oc-address-suggestion]` otherwise. */\n suggestionClassName?: string;\n}\n\n/**\n * Bitcoin-address `<input>` that, when the user is signed in via `oc_session`,\n * surfaces their address as a one-click suggestion on focus.\n *\n * Behaviour:\n * - On focus, if logged-in AND the typed value is a prefix of the session\n * address (or empty), show a small popover with `bc1q…7ke3` as a clickable\n * suggestion.\n * - Click / Enter on the suggestion fills the field with the full address.\n * - Down-arrow from the input highlights the suggestion; Up-arrow clears the\n * highlight; Escape closes the popover; clicking outside closes the popover.\n * - When the user types something that's no longer a prefix of the session\n * address, the popover hides itself out of the way.\n * - When the field already contains the session address exactly, no popover.\n *\n * Style-agnostic: minimal inline styles for positioning only. Style the parts\n * via `wrapperClassName` / `popoverClassName` / `suggestionClassName`, or via\n * the `[data-oc-address-input]`, `[data-oc-address-popover]`, and\n * `[data-oc-address-suggestion]` data attributes.\n */\nexport interface UseOcAddressSuggestionOptions {\n /** Current value of the input. */\n value: string;\n /** Called when the user selects the suggestion (or you may also call it from the input's onChange). */\n onValueChange: (value: string) => void;\n /** Label shown above the suggested address in the popover. Defaults to `use your address`. */\n suggestionLabel?: string;\n /** className applied to the suggestion popover. Style with `[data-oc-address-popover]` otherwise. */\n popoverClassName?: string;\n /** className applied to the suggestion button. Style with `[data-oc-address-suggestion]` otherwise. */\n suggestionClassName?: string;\n}\n\nexport interface UseOcAddressSuggestionReturn {\n /**\n * Props to spread onto your `<input>` element. Adds focus/blur/keydown\n * handlers and the combobox ARIA attributes. Combine with your existing\n * `value`/`onChange` props — this hook does NOT control them.\n */\n inputProps: {\n onFocus: (e: React.FocusEvent<HTMLInputElement>) => void;\n onBlur: (e: React.FocusEvent<HTMLInputElement>) => void;\n onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;\n role: 'combobox';\n 'aria-haspopup': 'listbox';\n 'aria-expanded': boolean;\n 'aria-controls': string | undefined;\n 'aria-activedescendant': string | undefined;\n autoComplete: 'off';\n spellCheck: false;\n };\n /**\n * The suggestion popover. Render directly after your `<input>`, inside a\n * `position: relative` container so the popover anchors below the input.\n * Returns `null` when there's no suggestion to show.\n */\n popover: React.ReactNode;\n}\n\n/**\n * Hook variant of `OcAddressInput`. Use when you want to keep your existing\n * styled `<input>` (e.g. shadcn `<Input>`) and just bolt on the\n * session-address suggestion behaviour.\n *\n * Wrap your input in a `position: relative` container, spread `inputProps`\n * onto the input, and render `{popover}` immediately after. The hook's\n * focus / blur / keydown handlers are composed via `inputProps` — they call\n * any handlers you've already passed to your input only when you wire them\n * yourself in addition to spreading `inputProps`.\n *\n * Example:\n * ```tsx\n * const { inputProps, popover } = useOcAddressSuggestion({\n * value: addr,\n * onValueChange: setAddr,\n * });\n * return (\n * <div className=\"relative\">\n * <Input\n * value={addr}\n * onChange={(e) => setAddr(e.target.value)}\n * {...inputProps}\n * placeholder=\"bc1q…\"\n * />\n * {popover}\n * </div>\n * );\n * ```\n */\n/**\n * The address to offer: the account's primary one, else the Bitcoin address it\n * shows. Consumer sites verify the session locally and have no primary address\n * (`primary_btc` is null), but a wallet sign-in shows its address.\n */\nfunction accountAddress(account: OcAccount | null | undefined): string | null {\n if (account?.primaryBtc) return account.primaryBtc;\n return account?.displayIdentity?.kind === 'btc' ? account.displayIdentity.value : null;\n}\n\nexport function useOcAddressSuggestion(\n options: UseOcAddressSuggestionOptions\n): UseOcAddressSuggestionReturn {\n const {\n value,\n onValueChange,\n suggestionLabel = 'use your address',\n popoverClassName,\n suggestionClassName,\n } = options;\n\n const { status, account } = useOcSession();\n const sessionAddress = status === 'authenticated' ? accountAddress(account) : null;\n\n const [open, setOpen] = React.useState(false);\n const [highlighted, setHighlighted] = React.useState(false);\n const blurTimer = React.useRef<number | null>(null);\n\n const listboxId = useUniqueId('oc-addr-listbox');\n const optionId = `${listboxId}-opt`;\n\n const valueMatchesSession =\n sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();\n const canSuggest =\n sessionAddress != null &&\n sessionAddress.length > 0 &&\n !valueMatchesSession &&\n isPrefixOf(value, sessionAddress);\n const showPopover = open && canSuggest;\n\n function selectSuggestion() {\n if (!sessionAddress) return;\n onValueChange(sessionAddress);\n setOpen(false);\n setHighlighted(false);\n }\n\n React.useEffect(() => {\n return () => {\n if (blurTimer.current != null) window.clearTimeout(blurTimer.current);\n };\n }, []);\n\n const inputProps: UseOcAddressSuggestionReturn['inputProps'] = {\n onFocus: () => {\n if (blurTimer.current != null) {\n window.clearTimeout(blurTimer.current);\n blurTimer.current = null;\n }\n setOpen(true);\n },\n onBlur: () => {\n blurTimer.current = window.setTimeout(() => {\n setOpen(false);\n setHighlighted(false);\n }, 120);\n },\n onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (!showPopover) return;\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n setHighlighted(true);\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n setHighlighted(false);\n } else if (e.key === 'Escape') {\n e.preventDefault();\n setOpen(false);\n setHighlighted(false);\n } else if (e.key === 'Enter' && highlighted) {\n e.preventDefault();\n selectSuggestion();\n }\n },\n role: 'combobox',\n 'aria-haspopup': 'listbox',\n 'aria-expanded': showPopover,\n 'aria-controls': showPopover ? listboxId : undefined,\n 'aria-activedescendant': highlighted ? optionId : undefined,\n autoComplete: 'off',\n spellCheck: false,\n };\n\n const popover = showPopover && sessionAddress ? (\n <div\n id={listboxId}\n role=\"listbox\"\n data-oc-address-popover=\"\"\n className={popoverClassName}\n style={{\n position: 'absolute',\n zIndex: 50,\n top: 'calc(100% + 4px)',\n left: 0,\n right: 0,\n }}\n onMouseDown={(e) => e.preventDefault()}\n >\n <button\n type=\"button\"\n id={optionId}\n role=\"option\"\n aria-selected={highlighted}\n data-oc-address-suggestion=\"\"\n data-highlighted={highlighted ? '' : undefined}\n className={suggestionClassName}\n onClick={selectSuggestion}\n onMouseEnter={() => setHighlighted(true)}\n onMouseLeave={() => setHighlighted(false)}\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: '0.75rem',\n width: '100%',\n textAlign: 'left',\n cursor: 'pointer',\n font: 'inherit',\n background: 'inherit',\n color: 'inherit',\n border: 0,\n padding: 'inherit',\n }}\n >\n <span data-oc-address-suggestion-label=\"\">{suggestionLabel}</span>\n <span\n data-oc-address-suggestion-value=\"\"\n style={{ fontFamily: 'ui-monospace, monospace' }}\n >\n {shortenAddressMid(sessionAddress)}\n </span>\n </button>\n </div>\n ) : null;\n\n return { inputProps, popover };\n}\n\nexport const OcAddressInput = React.forwardRef<HTMLInputElement, OcAddressInputProps>(\n function OcAddressInput(\n {\n value,\n onValueChange,\n suggestionLabel = 'use your address',\n wrapperClassName,\n popoverClassName,\n suggestionClassName,\n onFocus,\n onBlur,\n onKeyDown,\n ...rest\n },\n forwardedRef\n ): React.ReactElement {\n const { status, account } = useOcSession();\n const sessionAddress = status === 'authenticated' ? accountAddress(account) : null;\n\n const [open, setOpen] = React.useState(false);\n const [highlighted, setHighlighted] = React.useState(false);\n\n const innerRef = React.useRef<HTMLInputElement | null>(null);\n const setRef = (node: HTMLInputElement | null) => {\n innerRef.current = node;\n if (typeof forwardedRef === 'function') forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n };\n const blurTimer = React.useRef<number | null>(null);\n\n const listboxId = useUniqueId('oc-addr-listbox');\n const optionId = `${listboxId}-opt`;\n\n const valueMatchesSession =\n sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();\n const canSuggest =\n sessionAddress != null &&\n sessionAddress.length > 0 &&\n !valueMatchesSession &&\n isPrefixOf(value, sessionAddress);\n const showPopover = open && canSuggest;\n\n function selectSuggestion() {\n if (!sessionAddress) return;\n onValueChange(sessionAddress);\n setOpen(false);\n setHighlighted(false);\n // Re-focus so the next Tab moves on naturally.\n innerRef.current?.focus();\n }\n\n function handleFocus(e: React.FocusEvent<HTMLInputElement>) {\n if (blurTimer.current != null) {\n window.clearTimeout(blurTimer.current);\n blurTimer.current = null;\n }\n setOpen(true);\n onFocus?.(e);\n }\n\n function handleBlur(e: React.FocusEvent<HTMLInputElement>) {\n // Defer close so a click on the suggestion can register first.\n blurTimer.current = window.setTimeout(() => {\n setOpen(false);\n setHighlighted(false);\n }, 120);\n onBlur?.(e);\n }\n\n function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {\n if (showPopover) {\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n setHighlighted(true);\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n setHighlighted(false);\n } else if (e.key === 'Escape') {\n e.preventDefault();\n setOpen(false);\n setHighlighted(false);\n } else if (e.key === 'Enter' && highlighted) {\n e.preventDefault();\n selectSuggestion();\n }\n }\n onKeyDown?.(e);\n }\n\n React.useEffect(() => {\n return () => {\n if (blurTimer.current != null) window.clearTimeout(blurTimer.current);\n };\n }, []);\n\n return (\n <div\n data-oc-address-input=\"\"\n className={wrapperClassName}\n style={{ position: 'relative' }}\n >\n <input\n {...rest}\n ref={setRef}\n value={value}\n onChange={(e) => onValueChange(e.target.value)}\n onFocus={handleFocus}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-expanded={showPopover}\n aria-controls={showPopover ? listboxId : undefined}\n aria-activedescendant={highlighted ? optionId : undefined}\n autoComplete=\"off\"\n spellCheck={false}\n />\n {showPopover && sessionAddress && (\n <div\n id={listboxId}\n role=\"listbox\"\n data-oc-address-popover=\"\"\n className={popoverClassName}\n style={{\n position: 'absolute',\n zIndex: 50,\n top: 'calc(100% + 4px)',\n left: 0,\n right: 0,\n }}\n // Prevent the input from blurring before the click on the suggestion lands.\n onMouseDown={(e) => e.preventDefault()}\n >\n <button\n type=\"button\"\n id={optionId}\n role=\"option\"\n aria-selected={highlighted}\n data-oc-address-suggestion=\"\"\n data-highlighted={highlighted ? '' : undefined}\n className={suggestionClassName}\n onClick={selectSuggestion}\n onMouseEnter={() => setHighlighted(true)}\n onMouseLeave={() => setHighlighted(false)}\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: '0.75rem',\n width: '100%',\n textAlign: 'left',\n cursor: 'pointer',\n font: 'inherit',\n background: 'inherit',\n color: 'inherit',\n border: 'inherit',\n padding: 'inherit',\n }}\n >\n <span data-oc-address-suggestion-label=\"\">{suggestionLabel}</span>\n <span\n data-oc-address-suggestion-value=\"\"\n style={{ fontFamily: 'ui-monospace, monospace' }}\n >\n {shortenAddressMid(sessionAddress)}\n </span>\n </button>\n </div>\n )}\n </div>\n );\n }\n);\n"],"mappings":"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","names":["OcAddressInput"]}
@@ -0,0 +1,253 @@
1
+ import {
2
+ useOcSession
3
+ } from "./chunk-7ST4Q5KR.mjs";
4
+ import {
5
+ resolveConfig
6
+ } from "./chunk-VRLFZAGK.mjs";
7
+
8
+ // src/webauthn.tsx
9
+ import * as React from "react";
10
+ import {
11
+ startAuthentication,
12
+ startRegistration
13
+ } from "@simplewebauthn/browser";
14
+ function resolveAuthOrigin(opts) {
15
+ if (!opts) return resolveConfig(void 0).authOrigin;
16
+ return resolveConfig(opts).authOrigin;
17
+ }
18
+ function isAbortError(err) {
19
+ if (!(err instanceof Error)) return false;
20
+ return /NotAllowed|aborted|cancel|user cancelled/i.test(err.message);
21
+ }
22
+ function useWebAuthnRegister(opts) {
23
+ const authOrigin = resolveAuthOrigin(opts);
24
+ const [status, setStatus] = React.useState("idle");
25
+ const [error, setError] = React.useState(null);
26
+ const reset = React.useCallback(() => {
27
+ setStatus("idle");
28
+ setError(null);
29
+ }, []);
30
+ const register = React.useCallback(
31
+ async (args) => {
32
+ setError(null);
33
+ setStatus("requesting-options");
34
+ try {
35
+ const optsRes = await fetch(`${authOrigin}/api/auth/webauthn/register/options`, {
36
+ method: "POST",
37
+ credentials: "include",
38
+ headers: { "Content-Type": "application/json" },
39
+ body: JSON.stringify({ label: args?.label })
40
+ });
41
+ const optsBody = await optsRes.json();
42
+ if (!optsRes.ok || !optsBody.ok || !optsBody.options || !optsBody.challenge_token) {
43
+ const reason = optsBody.reason ?? `options_failed_${optsRes.status}`;
44
+ setError(new Error(reason));
45
+ setStatus("error");
46
+ return { ok: false, reason };
47
+ }
48
+ setStatus("authenticating");
49
+ let attestation;
50
+ try {
51
+ attestation = await startRegistration({ optionsJSON: optsBody.options });
52
+ } catch (e) {
53
+ const reason = isAbortError(e) ? "cancelled" : e.message;
54
+ setError(new Error(reason));
55
+ setStatus("error");
56
+ return { ok: false, reason };
57
+ }
58
+ setStatus("verifying");
59
+ const verifyRes = await fetch(`${authOrigin}/api/auth/webauthn/register/verify`, {
60
+ method: "POST",
61
+ credentials: "include",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify({
64
+ challenge_token: optsBody.challenge_token,
65
+ response: attestation,
66
+ label: args?.label
67
+ })
68
+ });
69
+ const verifyBody = await verifyRes.json();
70
+ if (!verifyRes.ok || !verifyBody.ok || !verifyBody.credential) {
71
+ const reason = verifyBody.reason ?? `verify_failed_${verifyRes.status}`;
72
+ setError(new Error(reason));
73
+ setStatus("error");
74
+ return { ok: false, reason };
75
+ }
76
+ setStatus("success");
77
+ return { ok: true, credential: verifyBody.credential };
78
+ } catch (e) {
79
+ const reason = e instanceof Error ? e.message : String(e);
80
+ setError(new Error(reason));
81
+ setStatus("error");
82
+ return { ok: false, reason };
83
+ }
84
+ },
85
+ [authOrigin]
86
+ );
87
+ return { status, error, register, reset };
88
+ }
89
+ function useWebAuthnList(opts) {
90
+ const authOrigin = resolveAuthOrigin(opts);
91
+ const { status: sessionStatus } = useOcSession();
92
+ const [credentials, setCredentials] = React.useState([]);
93
+ const [status, setStatus] = React.useState("loading");
94
+ const [error, setError] = React.useState(null);
95
+ const refetch = React.useCallback(async () => {
96
+ setError(null);
97
+ if (sessionStatus !== "authenticated") {
98
+ setCredentials([]);
99
+ setStatus("ready");
100
+ return;
101
+ }
102
+ setStatus("loading");
103
+ try {
104
+ const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials`, {
105
+ credentials: "include"
106
+ });
107
+ if (r.status === 401) {
108
+ setCredentials([]);
109
+ setStatus("ready");
110
+ return;
111
+ }
112
+ const j = await r.json();
113
+ if (!j.ok || !j.credentials) {
114
+ throw new Error(j.reason ?? "list_failed");
115
+ }
116
+ setCredentials(j.credentials);
117
+ setStatus("ready");
118
+ } catch (e) {
119
+ setError(e instanceof Error ? e : new Error(String(e)));
120
+ setStatus("error");
121
+ }
122
+ }, [authOrigin, sessionStatus]);
123
+ React.useEffect(() => {
124
+ void refetch();
125
+ }, [refetch]);
126
+ const rename = React.useCallback(
127
+ async (id, label) => {
128
+ try {
129
+ const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
130
+ method: "PATCH",
131
+ credentials: "include",
132
+ headers: { "Content-Type": "application/json" },
133
+ body: JSON.stringify({ label })
134
+ });
135
+ const j = await r.json();
136
+ if (!j.ok || !j.credential) {
137
+ const reason = j.reason ?? `rename_failed_${r.status}`;
138
+ setError(new Error(reason));
139
+ return { ok: false, reason };
140
+ }
141
+ await refetch();
142
+ return { ok: true, credential: j.credential };
143
+ } catch (e) {
144
+ const reason = e instanceof Error ? e.message : String(e);
145
+ setError(new Error(reason));
146
+ return { ok: false, reason };
147
+ }
148
+ },
149
+ [authOrigin, refetch]
150
+ );
151
+ const remove = React.useCallback(
152
+ async (id) => {
153
+ try {
154
+ const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
155
+ method: "DELETE",
156
+ credentials: "include"
157
+ });
158
+ const j = await r.json();
159
+ if (!j.ok) {
160
+ const reason = j.reason ?? `delete_failed_${r.status}`;
161
+ setError(new Error(reason));
162
+ return { ok: false, reason };
163
+ }
164
+ await refetch();
165
+ return { ok: true };
166
+ } catch (e) {
167
+ const reason = e instanceof Error ? e.message : String(e);
168
+ setError(new Error(reason));
169
+ return { ok: false, reason };
170
+ }
171
+ },
172
+ [authOrigin, refetch]
173
+ );
174
+ return { status, credentials, error, refetch, rename, remove };
175
+ }
176
+ function useStepUpAuth(opts) {
177
+ const authOrigin = resolveAuthOrigin(opts);
178
+ const { refresh } = useOcSession();
179
+ const [status, setStatus] = React.useState("idle");
180
+ const [error, setError] = React.useState(null);
181
+ const reset = React.useCallback(() => {
182
+ setStatus("idle");
183
+ setError(null);
184
+ }, []);
185
+ const stepUp = React.useCallback(
186
+ async (args) => {
187
+ setError(null);
188
+ setStatus("requesting-options");
189
+ try {
190
+ const optsRes = await fetch(`${authOrigin}/api/auth/webauthn/assertion/options`, {
191
+ method: "POST",
192
+ credentials: "include",
193
+ headers: { "Content-Type": "application/json" },
194
+ body: JSON.stringify({ purpose: args.purpose })
195
+ });
196
+ const optsBody = await optsRes.json();
197
+ if (!optsRes.ok || !optsBody.ok || !optsBody.options || !optsBody.challenge_token) {
198
+ const reason = optsBody.reason ?? `options_failed_${optsRes.status}`;
199
+ setError(new Error(reason));
200
+ setStatus("error");
201
+ return { ok: false, reason };
202
+ }
203
+ setStatus("authenticating");
204
+ let assertion;
205
+ try {
206
+ assertion = await startAuthentication({ optionsJSON: optsBody.options });
207
+ } catch (e) {
208
+ const reason = isAbortError(e) ? "cancelled" : e.message;
209
+ setError(new Error(reason));
210
+ setStatus("error");
211
+ return { ok: false, reason };
212
+ }
213
+ setStatus("verifying");
214
+ const verifyRes = await fetch(
215
+ `${authOrigin}/api/auth/webauthn/assertion/verify`,
216
+ {
217
+ method: "POST",
218
+ credentials: "include",
219
+ headers: { "Content-Type": "application/json" },
220
+ body: JSON.stringify({
221
+ challenge_token: optsBody.challenge_token,
222
+ response: assertion
223
+ })
224
+ }
225
+ );
226
+ const verifyBody = await verifyRes.json();
227
+ if (!verifyRes.ok || !verifyBody.ok || typeof verifyBody.step_up_at !== "number") {
228
+ const reason = verifyBody.reason ?? `verify_failed_${verifyRes.status}`;
229
+ setError(new Error(reason));
230
+ setStatus("error");
231
+ return { ok: false, reason };
232
+ }
233
+ await refresh();
234
+ setStatus("success");
235
+ return { ok: true, step_up_at: verifyBody.step_up_at };
236
+ } catch (e) {
237
+ const reason = e instanceof Error ? e.message : String(e);
238
+ setError(new Error(reason));
239
+ setStatus("error");
240
+ return { ok: false, reason };
241
+ }
242
+ },
243
+ [authOrigin, refresh]
244
+ );
245
+ return { status, error, stepUp, reset };
246
+ }
247
+
248
+ export {
249
+ useWebAuthnRegister,
250
+ useWebAuthnList,
251
+ useStepUpAuth
252
+ };
253
+ //# sourceMappingURL=chunk-BYAF2P2L.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/webauthn.tsx"],"sourcesContent":["/**\n * WebAuthn / passkey hooks for OrangeCheck consumer subdomains.\n *\n * Three hooks, all routing through the auth host (rpId='ochk.io'):\n *\n * - `useWebAuthnRegister()` — bind a hardware key to the account.\n * - `useWebAuthnList()` — read + rename + revoke registered keys.\n * - `useStepUpAuth()` — prove possession of a key before a\n * sensitive action; the auth host\n * re-issues the session cookie with a\n * fresh `step_up_at` claim.\n *\n * Consumers don't talk to navigator.credentials directly — these hooks\n * own the round-trip to ochk.io + the @simplewebauthn/browser ceremony.\n *\n * Server-side gating is one import away in auth-core's\n * `verifyStepUpClaim(payload, { max_age_secs })`.\n */\n\nimport * as React from 'react';\nimport {\n startAuthentication,\n startRegistration,\n type AuthenticationResponseJSON,\n type PublicKeyCredentialCreationOptionsJSON,\n type PublicKeyCredentialRequestOptionsJSON,\n type RegistrationResponseJSON,\n} from '@simplewebauthn/browser';\n\nimport { useOcSession } from './provider';\nimport { resolveConfig, type OcAuthConfig } from './types';\n\n// ─── Shared types ───────────────────────────────────────────────────────\n\nexport interface WebAuthnCredentialPublic {\n id: string;\n label: string;\n authenticator_type: 'platform' | 'cross-platform' | 'unknown';\n transports: string[];\n user_verified: boolean;\n created_at: string;\n last_used_at: string | null;\n}\n\nexport type WebAuthnRegisterStatus =\n | 'idle'\n | 'requesting-options'\n | 'authenticating'\n | 'verifying'\n | 'success'\n | 'error';\n\nexport type WebAuthnAssertionStatus = WebAuthnRegisterStatus;\nexport type WebAuthnListStatus = 'loading' | 'ready' | 'error';\n\ninterface UseHostOptions {\n /** Override the auth host origin. Defaults to https://ochk.io.\n * Same convention as <OcSessionProvider config={{ authOrigin }} />. */\n authOrigin?: string;\n}\n\nfunction resolveAuthOrigin(opts?: UseHostOptions | OcAuthConfig): string {\n if (!opts) return resolveConfig(undefined).authOrigin;\n return resolveConfig(opts as OcAuthConfig).authOrigin;\n}\n\ninterface OptionsResponse<T> {\n ok: boolean;\n options?: T;\n challenge_token?: string;\n reason?: string;\n}\n\nfunction isAbortError(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n return /NotAllowed|aborted|cancel|user cancelled/i.test(err.message);\n}\n\n// ─── useWebAuthnRegister ────────────────────────────────────────────────\n\nexport type WebAuthnRegisterResult =\n | { ok: true; credential: WebAuthnCredentialPublic }\n | { ok: false; reason: string };\n\nexport interface UseWebAuthnRegisterReturn {\n status: WebAuthnRegisterStatus;\n error: Error | null;\n register: (args?: { label?: string }) => Promise<WebAuthnRegisterResult>;\n reset: () => void;\n}\n\nexport function useWebAuthnRegister(opts?: UseHostOptions): UseWebAuthnRegisterReturn {\n const authOrigin = resolveAuthOrigin(opts);\n const [status, setStatus] = React.useState<WebAuthnRegisterStatus>('idle');\n const [error, setError] = React.useState<Error | null>(null);\n\n const reset = React.useCallback(() => {\n setStatus('idle');\n setError(null);\n }, []);\n\n const register = React.useCallback(\n async (args?: { label?: string }): Promise<WebAuthnRegisterResult> => {\n setError(null);\n setStatus('requesting-options');\n try {\n const optsRes = await fetch(`${authOrigin}/api/auth/webauthn/register/options`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ label: args?.label }),\n });\n const optsBody = (await optsRes.json()) as OptionsResponse<PublicKeyCredentialCreationOptionsJSON>;\n if (!optsRes.ok || !optsBody.ok || !optsBody.options || !optsBody.challenge_token) {\n const reason = optsBody.reason ?? `options_failed_${optsRes.status}`;\n setError(new Error(reason));\n setStatus('error');\n return { ok: false, reason };\n }\n setStatus('authenticating');\n let attestation: RegistrationResponseJSON;\n try {\n attestation = await startRegistration({ optionsJSON: optsBody.options });\n } catch (e) {\n const reason = isAbortError(e) ? 'cancelled' : (e as Error).message;\n setError(new Error(reason));\n setStatus('error');\n return { ok: false, reason };\n }\n setStatus('verifying');\n const verifyRes = await fetch(`${authOrigin}/api/auth/webauthn/register/verify`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n challenge_token: optsBody.challenge_token,\n response: attestation,\n label: args?.label,\n }),\n });\n const verifyBody = (await verifyRes.json()) as {\n ok: boolean;\n credential?: WebAuthnCredentialPublic;\n reason?: string;\n };\n if (!verifyRes.ok || !verifyBody.ok || !verifyBody.credential) {\n const reason = verifyBody.reason ?? `verify_failed_${verifyRes.status}`;\n setError(new Error(reason));\n setStatus('error');\n return { ok: false, reason };\n }\n setStatus('success');\n return { ok: true, credential: verifyBody.credential };\n } catch (e) {\n const reason = e instanceof Error ? e.message : String(e);\n setError(new Error(reason));\n setStatus('error');\n return { ok: false, reason };\n }\n },\n [authOrigin]\n );\n\n return { status, error, register, reset };\n}\n\n// ─── useWebAuthnList ────────────────────────────────────────────────────\n\nexport type WebAuthnRenameResult =\n | { ok: true; credential: WebAuthnCredentialPublic }\n | { ok: false; reason: string };\n\nexport type WebAuthnRemoveResult =\n | { ok: true }\n | { ok: false; reason: string };\n\nexport interface UseWebAuthnListReturn {\n status: WebAuthnListStatus;\n credentials: WebAuthnCredentialPublic[];\n error: Error | null;\n refetch: () => Promise<void>;\n rename: (id: string, label: string) => Promise<WebAuthnRenameResult>;\n remove: (id: string) => Promise<WebAuthnRemoveResult>;\n}\n\nexport function useWebAuthnList(opts?: UseHostOptions): UseWebAuthnListReturn {\n const authOrigin = resolveAuthOrigin(opts);\n const { status: sessionStatus } = useOcSession();\n const [credentials, setCredentials] = React.useState<WebAuthnCredentialPublic[]>([]);\n const [status, setStatus] = React.useState<WebAuthnListStatus>('loading');\n const [error, setError] = React.useState<Error | null>(null);\n\n const refetch = React.useCallback(async () => {\n setError(null);\n if (sessionStatus !== 'authenticated') {\n setCredentials([]);\n setStatus('ready');\n return;\n }\n setStatus('loading');\n try {\n const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials`, {\n credentials: 'include',\n });\n if (r.status === 401) {\n setCredentials([]);\n setStatus('ready');\n return;\n }\n const j = (await r.json()) as {\n ok: boolean;\n credentials?: WebAuthnCredentialPublic[];\n reason?: string;\n };\n if (!j.ok || !j.credentials) {\n throw new Error(j.reason ?? 'list_failed');\n }\n setCredentials(j.credentials);\n setStatus('ready');\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n }\n }, [authOrigin, sessionStatus]);\n\n React.useEffect(() => {\n void refetch();\n }, [refetch]);\n\n const rename = React.useCallback(\n async (id: string, label: string): Promise<WebAuthnRenameResult> => {\n try {\n const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ label }),\n });\n const j = (await r.json()) as {\n ok: boolean;\n credential?: WebAuthnCredentialPublic;\n reason?: string;\n };\n if (!j.ok || !j.credential) {\n const reason = j.reason ?? `rename_failed_${r.status}`;\n setError(new Error(reason));\n return { ok: false, reason };\n }\n await refetch();\n return { ok: true, credential: j.credential };\n } catch (e) {\n const reason = e instanceof Error ? e.message : String(e);\n setError(new Error(reason));\n return { ok: false, reason };\n }\n },\n [authOrigin, refetch]\n );\n\n const remove = React.useCallback(\n async (id: string): Promise<WebAuthnRemoveResult> => {\n try {\n const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {\n method: 'DELETE',\n credentials: 'include',\n });\n const j = (await r.json()) as { ok: boolean; reason?: string };\n if (!j.ok) {\n const reason = j.reason ?? `delete_failed_${r.status}`;\n setError(new Error(reason));\n return { ok: false, reason };\n }\n await refetch();\n return { ok: true };\n } catch (e) {\n const reason = e instanceof Error ? e.message : String(e);\n setError(new Error(reason));\n return { ok: false, reason };\n }\n },\n [authOrigin, refetch]\n );\n\n return { status, credentials, error, refetch, rename, remove };\n}\n\n// ─── useStepUpAuth ──────────────────────────────────────────────────────\n\nexport type WebAuthnStepUpResult =\n | { ok: true; step_up_at: number }\n | { ok: false; reason: string };\n\nexport interface UseStepUpAuthReturn {\n status: WebAuthnAssertionStatus;\n error: Error | null;\n stepUp: (args: { purpose: string }) => Promise<WebAuthnStepUpResult>;\n reset: () => void;\n}\n\n/**\n * Prove possession of a registered hardware key before a sensitive\n * action. On success the auth host sets a fresh oc_session cookie\n * carrying `step_up_at = <unix-now>`; the provider re-fetches the\n * session so `verifyStepUpClaim(payload, …)` flips to true immediately.\n *\n * Throws via `error` (not via `throw`) when:\n * - the user has no credentials registered (`no_credentials_registered`)\n * - the user cancels the platform prompt (`cancelled`)\n * - the assertion fails verification on the host (`verify_failed`,\n * `cloned_authenticator`, `credential_outside_allowlist`, …)\n *\n * Caller pattern:\n *\n * const { stepUp } = useStepUpAuth();\n * const { refresh } = useOcSession();\n * const r = await stepUp({ purpose: 'spend_over_1m' });\n * if (!r) return; // user cancelled or step-up failed\n * await refresh(); // pick up the new step_up_at\n * // …proceed with the sensitive action\n */\nexport function useStepUpAuth(opts?: UseHostOptions): UseStepUpAuthReturn {\n const authOrigin = resolveAuthOrigin(opts);\n const { refresh } = useOcSession();\n const [status, setStatus] = React.useState<WebAuthnAssertionStatus>('idle');\n const [error, setError] = React.useState<Error | null>(null);\n\n const reset = React.useCallback(() => {\n setStatus('idle');\n setError(null);\n }, []);\n\n const stepUp = React.useCallback(\n async (args: { purpose: string }): Promise<WebAuthnStepUpResult> => {\n setError(null);\n setStatus('requesting-options');\n try {\n const optsRes = await fetch(`${authOrigin}/api/auth/webauthn/assertion/options`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ purpose: args.purpose }),\n });\n const optsBody = (await optsRes.json()) as OptionsResponse<PublicKeyCredentialRequestOptionsJSON>;\n if (!optsRes.ok || !optsBody.ok || !optsBody.options || !optsBody.challenge_token) {\n const reason = optsBody.reason ?? `options_failed_${optsRes.status}`;\n setError(new Error(reason));\n setStatus('error');\n return { ok: false, reason };\n }\n setStatus('authenticating');\n let assertion: AuthenticationResponseJSON;\n try {\n assertion = await startAuthentication({ optionsJSON: optsBody.options });\n } catch (e) {\n const reason = isAbortError(e) ? 'cancelled' : (e as Error).message;\n setError(new Error(reason));\n setStatus('error');\n return { ok: false, reason };\n }\n setStatus('verifying');\n const verifyRes = await fetch(\n `${authOrigin}/api/auth/webauthn/assertion/verify`,\n {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n challenge_token: optsBody.challenge_token,\n response: assertion,\n }),\n }\n );\n const verifyBody = (await verifyRes.json()) as {\n ok: boolean;\n step_up_at?: number;\n reason?: string;\n };\n if (!verifyRes.ok || !verifyBody.ok || typeof verifyBody.step_up_at !== 'number') {\n const reason = verifyBody.reason ?? `verify_failed_${verifyRes.status}`;\n setError(new Error(reason));\n setStatus('error');\n return { ok: false, reason };\n }\n // Re-fetch the session so consumers see the fresh\n // step_up_at claim without a separate `refresh()` call.\n await refresh();\n setStatus('success');\n return { ok: true, step_up_at: verifyBody.step_up_at };\n } catch (e) {\n const reason = e instanceof Error ? e.message : String(e);\n setError(new Error(reason));\n setStatus('error');\n return { ok: false, reason };\n }\n },\n [authOrigin, refresh]\n );\n\n return { status, error, stepUp, reset };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,118 @@
1
+ import {
2
+ TAB_ADOPT_HASH
3
+ } from "./chunk-TJJNTITX.mjs";
4
+
5
+ // src/return-to.ts
6
+ var PREFIX = "oc:return-fragment:";
7
+ var HOLD_MS = 1e4;
8
+ var FRAGMENT_TTL_MS = 15 * 60 * 1e3;
9
+ function store() {
10
+ try {
11
+ return window.sessionStorage;
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+ function stripFragment(target) {
17
+ const cut = target.indexOf("#");
18
+ return cut < 0 ? target : target.slice(0, cut);
19
+ }
20
+ function keep(pathAndQuery, hash) {
21
+ if (hash.length < 2) return;
22
+ try {
23
+ store()?.setItem(PREFIX + pathAndQuery, JSON.stringify({ hash, at: Date.now() }));
24
+ } catch {
25
+ }
26
+ }
27
+ function withoutFragment(target) {
28
+ if (typeof window !== "undefined" && target.includes("#")) {
29
+ try {
30
+ const u = new URL(target, window.location.href);
31
+ if (u.origin === window.location.origin) keep(u.pathname + u.search, u.hash);
32
+ } catch {
33
+ }
34
+ }
35
+ return stripFragment(target);
36
+ }
37
+ function returnToHere() {
38
+ if (typeof window === "undefined") return "/";
39
+ const { pathname, search, hash } = window.location;
40
+ return withoutFragment(pathname + search + hash);
41
+ }
42
+ function keepFragmentOnReturnLinks() {
43
+ if (typeof document === "undefined") return () => {
44
+ };
45
+ const onClick = (e) => {
46
+ const { pathname, search, hash, href } = window.location;
47
+ if (hash.length < 2 || e.defaultPrevented) return;
48
+ const link = e.target?.closest?.("a[href]");
49
+ if (!link) return;
50
+ try {
51
+ const to = new URL(link.getAttribute("href") ?? "", href);
52
+ const back = to.searchParams.get("return_to");
53
+ if (!back) return;
54
+ const here = new URL(back, href);
55
+ if (here.origin === window.location.origin && here.pathname === pathname && here.search === search) {
56
+ keep(pathname + search, hash);
57
+ }
58
+ } catch {
59
+ }
60
+ };
61
+ document.addEventListener("click", onClick, true);
62
+ return () => document.removeEventListener("click", onClick, true);
63
+ }
64
+ function restoreReturnFragment() {
65
+ if (typeof window === "undefined") return false;
66
+ const s = store();
67
+ if (!s) return false;
68
+ const { pathname, search, hash } = window.location;
69
+ const key = PREFIX + pathname + search;
70
+ let raw;
71
+ try {
72
+ raw = s.getItem(key);
73
+ if (raw === null) return false;
74
+ s.removeItem(key);
75
+ } catch {
76
+ return false;
77
+ }
78
+ if (hash) return false;
79
+ try {
80
+ const kept = JSON.parse(raw);
81
+ if (typeof kept.hash !== "string" || !kept.hash.startsWith("#")) return false;
82
+ if (typeof kept.at !== "number" || Date.now() - kept.at > FRAGMENT_TTL_MS) return false;
83
+ const restored = pathname + search + kept.hash;
84
+ window.history.replaceState(window.history.state, "", restored);
85
+ holdFragment(pathname + search, kept.hash, restored);
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+ function holdFragment(pathAndQuery, hash, restored) {
92
+ const until = Date.now() + HOLD_MS;
93
+ const check = () => {
94
+ const l = window.location;
95
+ if (l.pathname + l.search !== pathAndQuery || l.hash === hash) {
96
+ if (l.pathname + l.search === pathAndQuery && Date.now() < until) {
97
+ window.setTimeout(check, 50);
98
+ }
99
+ return;
100
+ }
101
+ if (l.hash && l.hash !== TAB_ADOPT_HASH) return;
102
+ try {
103
+ window.history.replaceState(window.history.state, "", restored);
104
+ } catch {
105
+ }
106
+ };
107
+ window.setTimeout(check, 0);
108
+ }
109
+
110
+ export {
111
+ FRAGMENT_TTL_MS,
112
+ stripFragment,
113
+ withoutFragment,
114
+ returnToHere,
115
+ keepFragmentOnReturnLinks,
116
+ restoreReturnFragment
117
+ };
118
+ //# sourceMappingURL=chunk-CYR7J4UY.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/return-to.ts"],"sourcesContent":["/**\n * A return address never carries a fragment.\n *\n * A fragment can hold what a page means to keep in the browser: a share\n * link's envelope, a team invite's key. A return address travels in a query\n * string (`/sudo?return_to=…`, `/signin?return_to=…`), and a query reaches the\n * server, its logs and its analytics. So every return address this package\n * builds has its fragment taken off. When the browser leaves for one, the\n * fragment is kept in this tab's sessionStorage under the path it belongs\n * to, and OcSessionProvider puts it back when the browser lands on that path\n * again, before the page renders.\n *\n * It is kept at the moment of leaving, never while a page merely renders a\n * link: redirectToSudo and returnToHere keep it when called, and the provider\n * keeps it when a link carrying this page as its return_to is followed. What\n * is kept stays in the tab that had it, is used once, and is dropped after\n * {@link FRAGMENT_TTL_MS}. A return to another origin loses its fragment\n * rather than carry it: sessionStorage belongs to one origin.\n */\n\nimport { TAB_ADOPT_HASH } from './tab-session';\n\nconst PREFIX = 'oc:return-fragment:';\n\n/** How long a restored fragment is held against a router that resets the address after hydrating. */\nconst HOLD_MS = 10_000;\n\n/** Long enough for an email code or a wallet signature; short enough not to linger. */\nexport const FRAGMENT_TTL_MS = 15 * 60 * 1000;\n\nfunction store(): Storage | null {\n try {\n return window.sessionStorage;\n } catch {\n return null;\n }\n}\n\n/** `target` without its fragment. Pure: what to put in a URL, for a link rendered now and followed later. */\nexport function stripFragment(target: string): string {\n const cut = target.indexOf('#');\n return cut < 0 ? target : target.slice(0, cut);\n}\n\nfunction keep(pathAndQuery: string, hash: string): void {\n if (hash.length < 2) return;\n try {\n store()?.setItem(PREFIX + pathAndQuery, JSON.stringify({ hash, at: Date.now() }));\n } catch {\n // Storage refused: the fragment is dropped, never sent.\n }\n}\n\n/**\n * `target` without its fragment, for a browser about to leave for it. A\n * fragment on a same-origin target is kept for {@link restoreReturnFragment}.\n * Safe on the server, where it only cuts the fragment off.\n */\nexport function withoutFragment(target: string): string {\n if (typeof window !== 'undefined' && target.includes('#')) {\n try {\n const u = new URL(target, window.location.href);\n if (u.origin === window.location.origin) keep(u.pathname + u.search, u.hash);\n } catch {\n // Not a URL: nothing to keep.\n }\n }\n return stripFragment(target);\n}\n\n/**\n * This page as a return address: its path and query, never its fragment,\n * which is kept and restored on return. For a site that builds\n * `/signin?return_to=` itself:\n *\n * `/signin?return_to=${encodeURIComponent(returnToHere())}`\n */\nexport function returnToHere(): string {\n if (typeof window === 'undefined') return '/';\n const { pathname, search, hash } = window.location;\n return withoutFragment(pathname + search + hash);\n}\n\n/**\n * Keep this page's fragment when a link that names this page as its\n * `return_to` is followed: the session's signInUrl, an add-account link, a\n * site's own `/signin?return_to=` link. Installed by OcSessionProvider;\n * returns the uninstaller.\n */\nexport function keepFragmentOnReturnLinks(): () => void {\n if (typeof document === 'undefined') return () => {};\n const onClick = (e: MouseEvent) => {\n const { pathname, search, hash, href } = window.location;\n if (hash.length < 2 || e.defaultPrevented) return;\n const link = (e.target as Element | null)?.closest?.('a[href]');\n if (!link) return;\n try {\n const to = new URL(link.getAttribute('href') ?? '', href);\n const back = to.searchParams.get('return_to');\n if (!back) return;\n const here = new URL(back, href);\n if (\n here.origin === window.location.origin &&\n here.pathname === pathname &&\n here.search === search\n ) {\n keep(pathname + search, hash);\n }\n } catch {\n // Not a URL: not a return link.\n }\n };\n document.addEventListener('click', onClick, true);\n return () => document.removeEventListener('click', onClick, true);\n}\n\n/**\n * Put back the fragment a return address left behind, if the browser has\n * just landed on its path with no fragment of its own. Used once, then gone.\n * OcSessionProvider calls this before its children render; returns whether a\n * fragment was restored.\n */\nexport function restoreReturnFragment(): boolean {\n if (typeof window === 'undefined') return false;\n const s = store();\n if (!s) return false;\n const { pathname, search, hash } = window.location;\n const key = PREFIX + pathname + search;\n let raw: string | null;\n try {\n raw = s.getItem(key);\n if (raw === null) return false;\n s.removeItem(key);\n } catch {\n return false;\n }\n // The address brought its own fragment: that one wins.\n if (hash) return false;\n try {\n const kept = JSON.parse(raw) as { hash?: unknown; at?: unknown };\n if (typeof kept.hash !== 'string' || !kept.hash.startsWith('#')) return false;\n if (typeof kept.at !== 'number' || Date.now() - kept.at > FRAGMENT_TTL_MS) return false;\n const restored = pathname + search + kept.hash;\n window.history.replaceState(window.history.state, '', restored);\n holdFragment(pathname + search, kept.hash, restored);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Next's pages router captures the address before the app runs, and on a\n * page with a query or a dynamic route it replaces the address with that\n * capture once it has hydrated, which takes the restored fragment off again.\n * Put it back if that happens, once, while the browser stays on the page and\n * nothing else has set a fragment.\n */\nfunction holdFragment(pathAndQuery: string, hash: string, restored: string): void {\n const until = Date.now() + HOLD_MS;\n const check = () => {\n const l = window.location;\n if (l.pathname + l.search !== pathAndQuery || l.hash === hash) {\n if (l.pathname + l.search === pathAndQuery && Date.now() < until) {\n window.setTimeout(check, 50);\n }\n return;\n }\n if (l.hash && l.hash !== TAB_ADOPT_HASH) return;\n try {\n window.history.replaceState(window.history.state, '', restored);\n } catch {\n // The page keeps what it read on mount; only the address bar differs.\n }\n };\n window.setTimeout(check, 0);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,23 @@
1
+ // src/appearance.ts
2
+ var SIGNIN_FONT = "var(--oc-signin-font, ui-monospace, SFMono-Regular, monospace)";
3
+ var SIGNIN_CASE = "var(--oc-signin-case, uppercase)";
4
+ var signinTracking = (em) => `var(--oc-signin-tracking, ${em})`;
5
+ var signinSize = (px) => `calc(${px}px * var(--oc-signin-scale, 1))`;
6
+ var signinRadius = (px) => `var(--oc-signin-radius, ${px}px)`;
7
+ var SIGNIN_BUTTON_RADIUS = "var(--oc-signin-button-radius, 6px)";
8
+ var SIGNIN_STRONG = "var(--oc-signin-strong, 600)";
9
+ var SIGNIN_ARROW = "var(--oc-signin-arrow, inline)";
10
+ var signinRoleSize = (role, fallback) => `var(--oc-signin-${role}-size, ${fallback})`;
11
+
12
+ export {
13
+ SIGNIN_FONT,
14
+ SIGNIN_CASE,
15
+ signinTracking,
16
+ signinSize,
17
+ signinRadius,
18
+ SIGNIN_BUTTON_RADIUS,
19
+ SIGNIN_STRONG,
20
+ SIGNIN_ARROW,
21
+ signinRoleSize
22
+ };
23
+ //# sourceMappingURL=chunk-H2TXOCXH.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/appearance.ts"],"sourcesContent":["import type * as React from 'react';\n\n/*\n * Every type and shape choice below reads a CSS variable whose fallback is the\n * family look, so a site with a design of its own (ochk.app) restyles the\n * ceremony by setting variables on an ancestor, never by forking it. Family\n * sites set none and render exactly as before.\n *\n * --oc-signin-font labels, buttons, fields (monospace)\n * --oc-signin-case labels and buttons (uppercase)\n * --oc-signin-tracking letter-spacing of labels (per label)\n * --oc-signin-scale multiplies the 10–12px label sizes (1)\n * --oc-signin-radius fields and notices (6px; 4px for the add notice)\n * --oc-signin-button-radius buttons (6px)\n * --oc-signin-strong weight of the submit text (600)\n * --oc-signin-arrow display of the → after a submit label (inline)\n *\n * Sizes by role. Each falls back to the family size, so a site that sets none\n * renders as before; app-design sets its own body, hint, label and button sizes.\n *\n * --oc-signin-text-size the lead under a tab, notices, the link-also row\n * --oc-signin-hint-size hints, errors, the \"or\" between blocks\n * --oc-signin-label-size field labels and the method tabs\n * --oc-signin-button-size the submit button and the text buttons beside it\n *\n * Copy is written in sentence case. The family's uppercase transform shows it\n * as before; a site that sets the case to `none` reads it as written.\n */\nexport const SIGNIN_FONT = 'var(--oc-signin-font, ui-monospace, SFMono-Regular, monospace)';\nexport const SIGNIN_CASE = 'var(--oc-signin-case, uppercase)' as React.CSSProperties['textTransform'];\nexport const signinTracking = (em: string): string => `var(--oc-signin-tracking, ${em})`;\nexport const signinSize = (px: number): string => `calc(${px}px * var(--oc-signin-scale, 1))`;\nexport const signinRadius = (px: number): string => `var(--oc-signin-radius, ${px}px)`;\nexport const SIGNIN_BUTTON_RADIUS = 'var(--oc-signin-button-radius, 6px)';\nexport const SIGNIN_STRONG = 'var(--oc-signin-strong, 600)' as React.CSSProperties['fontWeight'];\nexport const SIGNIN_ARROW = 'var(--oc-signin-arrow, inline)' as React.CSSProperties['display'];\n\nexport type SigninRole = 'text' | 'hint' | 'label' | 'button';\n/** A role's size: the site's, else `fallback`, the family size for that element. */\nexport const signinRoleSize = (role: SigninRole, fallback: string): string =>\n `var(--oc-signin-${role}-size, ${fallback})`;\n"],"mappings":";AA4BO,IAAM,cAAc;AACpB,IAAM,cAAc;AACpB,IAAM,iBAAiB,CAAC,OAAuB,6BAA6B,EAAE;AAC9E,IAAM,aAAa,CAAC,OAAuB,QAAQ,EAAE;AACrD,IAAM,eAAe,CAAC,OAAuB,2BAA2B,EAAE;AAC1E,IAAM,uBAAuB;AAC7B,IAAM,gBAAgB;AACtB,IAAM,eAAe;AAIrB,IAAM,iBAAiB,CAAC,MAAkB,aAC7C,mBAAmB,IAAI,UAAU,QAAQ;","names":[]}