@orangecheck/auth-client 2.33.3 → 2.34.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-7ST4Q5KR.mjs +346 -0
  6. package/dist/chunk-7ST4Q5KR.mjs.map +1 -0
  7. package/dist/chunk-7UYPMTAV.mjs +652 -0
  8. package/dist/chunk-7UYPMTAV.mjs.map +1 -0
  9. package/dist/chunk-BYAF2P2L.mjs +253 -0
  10. package/dist/chunk-BYAF2P2L.mjs.map +1 -0
  11. package/dist/chunk-CYR7J4UY.mjs +118 -0
  12. package/dist/chunk-CYR7J4UY.mjs.map +1 -0
  13. package/dist/chunk-D3HXJ64H.mjs +914 -0
  14. package/dist/chunk-D3HXJ64H.mjs.map +1 -0
  15. package/dist/chunk-FDWEXEZL.mjs +490 -0
  16. package/dist/chunk-FDWEXEZL.mjs.map +1 -0
  17. package/dist/chunk-H2TXOCXH.mjs +23 -0
  18. package/dist/chunk-H2TXOCXH.mjs.map +1 -0
  19. package/dist/chunk-IQVBEYSW.mjs +1172 -0
  20. package/dist/chunk-IQVBEYSW.mjs.map +1 -0
  21. package/dist/chunk-TJJNTITX.mjs +187 -0
  22. package/dist/chunk-TJJNTITX.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 +12 -11
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +112 -4000
  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 +21 -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/signin.tsx"],"sourcesContent":["/**\n * @orangecheck/auth-client · OcSignIn\n *\n * In-place dual-path signin component for every `X.ochk.io` consumer site.\n * Calls the cross-subdomain auth host (`ochk.io`) via family-CORS so the\n * `.ochk.io` session cookie lands without any redirect.\n *\n * Two paths, one component:\n *\n * 1. Bitcoin wallet · BIP-322\n * - Paste address, in-page wallet extension signs a challenge\n * - POSTs { message, signature, expectedNonce, expectedAudience,\n * expectedPurpose } to ochk.io/api/auth/signin\n * - Identity is `did:oc:<rand>` linked to the BTC address\n *\n * 2. Email · OTP\n * - Enter email → ochk.io/api/auth/email-otp/start delivers a code\n * - Enter code → ochk.io/api/auth/email-otp/verify lands the cookie\n * - Identity is `did:oc:<rand>`. No wallet is created here: a Fedimint\n * wallet is provisioned client-side on first /me visit, and its sats\n * are held by the FEDERATION'S GUARDIANS — not by OrangeCheck, which\n * coordinates and never custodies (see oc-www's email-otp/verify\n * route). Naming the custodian is family invariant 8; the old wording\n * here and in the form copy said \"a custodial wallet\" with no subject,\n * which reads as OC holding it and contradicts the charter.\n *\n * **Both paths resolve to the same did:oc canonical identity** via the\n * auth host's account-linking logic — a user who started with email can\n * later link their BTC address (and vice versa). One identity, two doors,\n * persistent across every family subdomain.\n *\n * On success, the component **hard-navigates** (window.location.assign)\n * rather than calling onSuccess + relying on the consumer to route. This\n * is non-negotiable: OcSessionProvider does not auto-refetch /api/auth/me\n * on client-side route changes, so a soft redirect leaves it with stale\n * `status: 'anonymous'`. Hard nav forces the provider to remount with\n * the fresh cookie. Override via onSuccess if you must.\n */\n\nimport * as React from 'react';\n\nimport { LinkPromptStep } from './linked-identities';\nimport { stripFragment } from './return-to';\nimport { clearTabSession } from './tab-session';\nimport type { OcAccount } from './types';\nimport {\n SIGNIN_ARROW,\n SIGNIN_BUTTON_RADIUS,\n SIGNIN_CASE,\n SIGNIN_FONT,\n SIGNIN_STRONG,\n signinRadius,\n signinRoleSize,\n signinSize,\n signinTracking,\n} from './appearance';\n\n/* --- props --- */\n\ninterface OcSignInBaseProps {\n /**\n * Consumer-site audience URL passed as `expectedAudience` on the\n * BIP-322 challenge. e.g. `'https://vault.ochk.io'`. Required —\n * the challenge nonce is bound to this audience.\n */\n audience: string;\n /**\n * Where to navigate after success. Defaults to `'/'`.\n * Open-redirect-safe: a same-origin path (must start with `/`, NOT\n * `//`) or an absolute `https://` URL on `ochk.io` / `*.ochk.io` —\n * the add-another-account flow starts on a consumer subdomain and\n * must round-trip back to it, including through the OAuth provider\n * hop. Anything else is ignored. When omitted, the component reads\n * `?return_to=` (or `?next=`) from the page URL — so the auth\n * host's `/signin?add=1&return_to=…` entry point Just Works, the\n * same way the `add` prop auto-detects `?add=1`.\n */\n returnTo?: string;\n /**\n * Override the default hard-navigation behavior. When provided,\n * called with the account and the session JWT. The component will\n * NOT navigate — the caller is responsible. Use for custom\n * post-signin routing — e.g. /popup/signin postMessages\n * `{ account, token }` to its opener so a cross-domain integrator\n * (different eTLD+1 from .ochk.io, which the HttpOnly cookie can't\n * reach) can verify the session via JWKS.\n */\n onSuccess?: (account: OcAccount, token?: string) => void;\n /**\n * Async post-success routing. When provided (and `onSuccess` is\n * not), the component awaits `resolveReturnTo(account)` and\n * hard-navigates to the result instead of the static `returnTo`.\n *\n * This is the seam that lets a site keep persona-aware routing\n * without forking the ceremony — e.g. me.ochk.io resolves\n * `/api/me/intent` and routes to `/me/developer` | `/me/operator`\n * | `/me`. The returned value is open-redirect-checked exactly\n * like `returnTo` (same-origin paths only); on a resolver throw\n * the component falls back to the static `returnTo`.\n */\n resolveReturnTo?: (account: OcAccount) => string | Promise<string>;\n /**\n * Override the auth host. Defaults to `'https://ochk.io'`. For\n * preview / dev / staging only.\n */\n authOrigin?: string;\n /**\n * Initial visible tab. Defaults to `'email'` (since {@link providersFirst}\n * is on by default); resolves to `'wallet'` only when\n * `providersFirst={false}` and this is left unspecified.\n */\n initialPath?: 'wallet' | 'email';\n /**\n * Re-order the ceremony so the third-party providers (Google / GitHub)\n * render **above** the wallet + email panel, and the email path is the\n * default active tab. **On by default — the family standard.** A\n * first-time, non-Bitcoiner visitor sees the most familiar on-ramp\n * (Google / GitHub / email one-time code) first; the BIP-322 wallet path\n * stays one tab away, reframed as the most-sovereign option.\n *\n * Pass `providersFirst={false}` for the legacy bitcoin-first ordering.\n * Honors family rule #3 — email / OAuth is the easy bridge, the Bitcoin\n * address remains the canonical identity it resolves to.\n */\n providersFirst?: boolean;\n /**\n * Disable one of the two paths. Default is both enabled.\n * Useful for B2B-only sites that don't want to expose email-OTP.\n */\n paths?: { wallet?: boolean; email?: boolean };\n /**\n * Whether to show the \"also link my other identity\" checkbox on the\n * sign-in form. **On by default.** The checkbox is optional and\n * unchecked by default; it is shown on both the wallet and email\n * paths. If the user ticks it, the complementary identity's link\n * ceremony (BIP-322 for a wallet, OTP for an email) runs inline\n * immediately after a successful sign-in — before `onSuccess` /\n * `returnTo`, so it composes with custom routing — because the\n * sign-in just proved one credential and the link ceremony proves\n * the second. Pass `linkPrompt={false}` to omit the checkbox.\n */\n linkPrompt?: boolean;\n /**\n * Multi-account · when `true`, the sign-in is performed in \"add\"\n * mode: the resulting session is appended to the browser's existing\n * roster instead of replacing it. The previously-active account\n * stays signed in and remains a switch target on the auth host. Off\n * by default (back-compat). The component also reads `?add=1` from\n * the URL search params and treats it the same — so the auth host's\n * `/signin?add=1` entry point Just Works without any extra prop.\n *\n * The `add` flag is forwarded as a body field to the host's\n * `/api/auth/signin` and `/api/auth/email-otp/verify`; if the host\n * hasn't deployed the multi-account migration yet it silently\n * ignores the field and the call falls back to ordinary signin.\n */\n add?: boolean;\n /** className for the outer container. */\n className?: string;\n}\n\n/**\n * Where the ceremony's session goes.\n *\n * `family` (default): a `*.ochk.io` site. The auth host sets the shared\n * `.ochk.io` cookie and the component navigates.\n *\n * `first-party`: orangecheck's own site on another domain (ochk.app). Every\n * call to the auth host omits credentials, so the family cookie is neither\n * sent nor set; the host answers with a token bound to `audience`, handed to\n * `onSuccess` for the site to turn into its own cookie. No add-account or\n * link prompts (there is no family session to add to), and provider buttons\n * appear only when `providerHref` says where the site starts each one.\n */\nexport type OcSignInProps = OcSignInBaseProps &\n (\n | { mode?: 'family'; providerHref?: undefined }\n | {\n mode: 'first-party';\n onSuccess: (account: OcAccount, token?: string) => void;\n /** The site's own start URL for a provider (`google`, `github`). */\n providerHref?: (providerId: string) => string;\n }\n );\n\n/* --- shared types --- */\n\ninterface SigninJsonOk {\n ok: true;\n account: OcAccount;\n /**\n * The session JWT, echoed alongside the `Set-Cookie`. Cross-domain\n * integrators capture this to verify the session via JWKS — the\n * HttpOnly `.ochk.io` cookie never reaches their origin. Family\n * `.ochk.io` sites ignore it and rely on the cookie.\n */\n token?: string;\n}\n\ninterface SigninJsonErr {\n ok?: false;\n reason?: string;\n error?: string;\n issues?: unknown;\n}\n\ntype SigninJson = SigninJsonOk | SigninJsonErr;\n\n/* --- helpers --- */\n\n/**\n * A relative path is same-origin only if it RESOLVES to this origin.\n *\n * Prefix checks are not enough. `startsWith('/') && !startsWith('//')` accepts\n * `/\\evil.example`, and the WHATWG URL parser treats a backslash as a slash in\n * special schemes, so the browser resolves it to `https://evil.example/`:\n *\n * new URL('/\\evil.example', 'https://attest.ochk.io').href\n * -> 'https://evil.example/'\n *\n * Since hardNavigate() below hands its argument to window.location.assign,\n * that was a post-authentication open redirect on every consumer of this\n * package — the user completes a real sign-in on a real ochk.io domain and\n * lands on an attacker's page. `\\t`, `\\n` and `\\r` are stripped by the parser\n * too, so `/\\t/evil.example` is the same trick with different bytes.\n *\n * So instead of blacklisting characters, resolve and compare: whatever the URL\n * parser makes of the input is what the browser will navigate to, and that is\n * the thing that has to be same-origin.\n */\nfunction isSameOriginPath(candidate: string, origin: string): boolean {\n try {\n return new URL(candidate, origin).origin === origin;\n } catch {\n return false;\n }\n}\n\n/** Origin to resolve relative candidates against. Any absolute https origin\n * works for the comparison; on the server there is no window to read. */\nfunction resolutionOrigin(): string {\n return typeof window === 'undefined' ? 'https://ochk.io' : window.location.origin;\n}\n\nexport function safeReturnTo(input: string | undefined): string {\n const candidate = input ?? '/';\n if (typeof candidate !== 'string') return '/';\n if (!candidate.startsWith('/')) return '/';\n return isSameOriginPath(candidate, resolutionOrigin()) ? candidate : '/';\n}\n\n/**\n * Family-aware return target. Accepts a same-origin relative path\n * (exactly like {@link safeReturnTo}) OR an absolute `https://` URL on\n * `ochk.io` / `*.ochk.io` — mirroring the auth host's own post-signin\n * redirect allowlist. Returns `undefined` when the input is neither, so\n * callers can layer fallbacks.\n */\nexport function familyReturnTarget(input: string | undefined | null): string | undefined {\n if (typeof input !== 'string' || input.length === 0) return undefined;\n // Same-origin relative path — resolved, not prefix-matched. See\n // isSameOriginPath for why `/\\evil.example` defeated the old check.\n if (input.startsWith('/') && isSameOriginPath(input, resolutionOrigin())) {\n return input;\n }\n try {\n const u = new URL(input);\n if (u.protocol !== 'https:') return undefined;\n const host = u.hostname.toLowerCase();\n if (host === 'ochk.io' || host.endsWith('.ochk.io')) return u.toString();\n } catch {\n // not an absolute URL either\n }\n return undefined;\n}\n\nfunction hardNavigate(target: string): void {\n if (typeof window === 'undefined') return;\n window.location.assign(target);\n}\n\n/* --- failures people can act on --- */\n\n/**\n * The auth host answers with machine reasons. Its /api/auth/signin says it\n * exposes them \"so the /signin UI can give an actionable message\", and this\n * form printed them verbatim instead: typing \"hello\" as an address showed\n * `bad_request`. Each reason maps to what happened and what to do next.\n */\nconst SIGNIN_ERRORS: Record<string, string> = {\n bad_request: \"That doesn't look like a Bitcoin address.\",\n invalid_challenge:\n \"That signature doesn't match this address. Sign with the wallet that holds it.\",\n sig_invalid: \"That signature doesn't match this address. Sign with the wallet that holds it.\",\n sig_unsupported_scheme:\n \"We can't check this wallet's signature format yet. Try another wallet, or use email.\",\n expired: 'That request timed out. Try again for a fresh one.',\n not_yet_valid: 'That request is not valid yet. Check your device clock, then try again.',\n nonce_mismatch: \"That request didn't match. Try again for a fresh one.\",\n audience_mismatch: \"That request didn't match this site. Reload the page and try again.\",\n purpose_mismatch: \"That request didn't match. Try again for a fresh one.\",\n malformed: \"That signature couldn't be read. Copy it again, whole.\",\n cross_site_blocked: 'Your browser blocked this request. Reload the page and try again.',\n rate_limited: 'Too many tries. Wait a minute, then try again.',\n invalid_email: \"That email address doesn't look right.\",\n mailer_unavailable: \"We couldn't send the email right now. Try again in a minute.\",\n mailer_error: \"We couldn't send the email right now. Try again in a minute.\",\n wrong_code: \"That code isn't right. Check the newest email from us.\",\n replay: 'That code was already used. Send a new one.',\n invalid_token: 'That code is no longer valid. Send a new one.',\n email_mismatch: 'That code was sent to a different address. Send a new one.',\n};\n\nconst EMAIL_ERRORS: Record<string, string> = {\n expired: 'That code expired. Send a new one.',\n};\n\n/**\n * `bad_request` means the host's body schema failed. Fetching a challenge\n * that is the address; submitting a signature it is the pasted signature\n * (under 10 characters, say), which is not an address problem at all.\n */\nconst SUBMIT_ERRORS: Record<string, string> = {\n bad_request: \"That signature couldn't be read. Copy it again, whole.\",\n};\n\nconst NETWORK_ERROR = \"We couldn't reach the server. Check your connection and try again.\";\n\nexport function humanSigninError(\n reason: string,\n path: 'wallet' | 'email',\n step: 'challenge' | 'submit' = 'challenge'\n): string {\n // A wallet extension's own message (\"User rejected the request\") is prose already.\n if (!/^[a-z0-9_]+$/.test(reason)) return reason;\n if (path === 'wallet' && step === 'submit' && SUBMIT_ERRORS[reason]) return SUBMIT_ERRORS[reason];\n if (path === 'email' && EMAIL_ERRORS[reason]) return EMAIL_ERRORS[reason];\n return (\n SIGNIN_ERRORS[reason] ??\n (path === 'email'\n ? \"That didn't work. Try again, or send a new code.\"\n : \"That didn't work. Try again.\")\n );\n}\n\n/**\n * A failed fetch (\"Failed to fetch\") or an HTML error page parsed as JSON\n * (a SyntaxError) is not prose meant for a person; everything else thrown\n * here carries a host reason code or a wallet's own message.\n */\nfunction failureMessage(err: unknown, path: 'wallet' | 'email', step: 'challenge' | 'submit'): string {\n if (err instanceof TypeError || err instanceof SyntaxError) return NETWORK_ERROR;\n return humanSigninError(err instanceof Error ? err.message : 'failed', path, step);\n}\n\nconst BECH32_ADDRESS = /^(bc1|tb1|bcrt1)[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{8,87}$/i;\nconst BASE58_ADDRESS = /^[123mn][1-9A-HJ-NP-Za-km-z]{25,34}$/;\n\n/**\n * Shape check only, so a typo fails here with a clear message instead of\n * fetching a challenge for it and then blaming a missing wallet extension.\n * The host still verifies the signature against the real address.\n */\nexport function looksLikeBitcoinAddress(input: string): boolean {\n const s = input.trim();\n return BECH32_ADDRESS.test(s) || BASE58_ADDRESS.test(s);\n}\n\n/* --- main component --- */\n\nexport function OcSignIn({\n audience,\n returnTo,\n onSuccess,\n resolveReturnTo,\n linkPrompt: linkPromptProp = true,\n add: addProp,\n authOrigin = 'https://ochk.io',\n initialPath,\n providersFirst = true,\n paths,\n className,\n mode,\n providerHref,\n}: OcSignInProps): React.ReactElement {\n const walletEnabled = paths?.wallet ?? true;\n const emailEnabled = paths?.email ?? true;\n const firstParty = mode === 'first-party';\n // First-party: the family cookie must be neither sent nor set.\n const credentials: RequestCredentials = firstParty ? 'omit' : 'include';\n const linkPrompt = linkPromptProp && !firstParty;\n\n // Return target · prop wins; otherwise honor `?return_to=` / `?next=`\n // from the URL — the auth host's `/signin?add=1&return_to=…` entry\n // point needs the OAuth provider hop to carry the same target the\n // embedding page honors (the add-another-account fix: a vault user\n // adding an account via Google must land back on vault, not on the\n // host's homepage). Absolute https://*.ochk.io URLs are allowed;\n // everything else clamps to `/`.\n const [resolvedReturn, setResolvedReturn] = React.useState<string>(\n () => familyReturnTarget(returnTo) ?? safeReturnTo(returnTo)\n );\n React.useEffect(() => {\n const fromProp = familyReturnTarget(returnTo);\n if (fromProp) {\n setResolvedReturn(fromProp);\n return;\n }\n if (typeof window === 'undefined') return;\n const q = new URLSearchParams(window.location.search);\n const fromQuery =\n familyReturnTarget(q.get('return_to')) ?? familyReturnTarget(q.get('next'));\n setResolvedReturn(fromQuery ?? '/');\n }, [returnTo]);\n\n // Multi-account · prop wins; otherwise honor `?add=1` from the URL\n // so the auth host's /signin?add=1 entry point doesn't need any\n // extra wiring on every consumer's sign-in page.\n const [addMode, setAddMode] = React.useState<boolean>(Boolean(addProp) && !firstParty);\n React.useEffect(() => {\n if (typeof window === 'undefined') return;\n if (firstParty) {\n setAddMode(false);\n return;\n }\n if (addProp !== undefined) {\n setAddMode(Boolean(addProp));\n return;\n }\n setAddMode(new URLSearchParams(window.location.search).get('add') === '1');\n }, [addProp, firstParty]);\n\n // When providers lead the ceremony (ochk.io homepage), the email path\n // is the friendlier default tab; otherwise the canonical bitcoin-first\n // default holds. An explicit `initialPath` always wins.\n const [path, setPath] = React.useState<'wallet' | 'email'>(\n initialPath ?? (providersFirst ? 'email' : 'wallet')\n );\n // Set once sign-in succeeds and the account is missing its\n // complementary identity — the component then renders the focused\n // link step (LinkPromptStep) before running `proceed`.\n const [signedIn, setSignedIn] = React.useState<{\n method: 'btc' | 'email';\n didOc: string;\n proceed: () => void;\n } | null>(null);\n // The optional \"also link my other identity\" checkbox on the form.\n const [linkAlso, setLinkAlso] = React.useState(false);\n // Set when the user is bounced back here after a failed provider\n // (e.g. Google) sign-in — the auth host redirects to\n // `/signin?oauth_error=…`.\n const [oauthError, setOauthError] = React.useState(false);\n React.useEffect(() => {\n if (typeof window === 'undefined') return;\n if (new URLSearchParams(window.location.search).has('oauth_error')) {\n setOauthError(true);\n }\n }, []);\n\n const navigate = React.useCallback(\n async (account: OcAccount) => {\n if (resolveReturnTo) {\n try {\n const resolved = familyReturnTarget(await resolveReturnTo(account));\n hardNavigate(resolved ?? resolvedReturn);\n return;\n } catch {\n // resolver failed — fall through to the static returnTo\n }\n }\n hardNavigate(resolvedReturn);\n },\n [resolveReturnTo, resolvedReturn]\n );\n\n const handleSuccess = React.useCallback(\n async (account: OcAccount, token: string | undefined, via: 'wallet' | 'email') => {\n // The post-sign-in handoff — custom `onSuccess`, else navigation.\n const proceed = () => {\n // Per-tab · the user just completed a ceremony IN this tab —\n // it must adopt the new identity, not keep a stale pin.\n clearTabSession();\n if (onSuccess) onSuccess(account, token);\n else void navigate(account);\n };\n\n // Linking is opt-in via the form checkbox. Unchecked (or the\n // checkbox suppressed) → ordinary sign-in.\n if (!linkPrompt || !linkAlso) {\n proceed();\n return;\n }\n\n // Checked: run the complementary identity's link ceremony now.\n // /api/auth/me resolves the did:oc the BIP-322 link challenge is\n // bound to; a failure here must not strand the signed-in user.\n try {\n const meRes = await fetch(`${authOrigin}/api/auth/me`, {\n credentials: 'include',\n headers: { Accept: 'application/json' },\n });\n const me = meRes.ok\n ? ((await meRes.json()) as { account?: { did_oc?: string } })\n : null;\n const didOc = me?.account?.did_oc;\n if (didOc) {\n setSignedIn({\n method: via === 'email' ? 'btc' : 'email',\n didOc,\n proceed,\n });\n return;\n }\n } catch {\n // /api/auth/me unreachable — proceed without the link step.\n }\n proceed();\n },\n [onSuccess, linkPrompt, linkAlso, navigate, authOrigin]\n );\n\n if (!walletEnabled && !emailEnabled) {\n return (\n <div\n className={className}\n data-oc-signin=\"\"\n style={{\n padding: '1rem',\n border: '1px solid var(--border, #27272a)',\n color: 'var(--foreground, #fafafa)',\n fontFamily: SIGNIN_FONT,\n fontSize: signinSize(12),\n }}\n >\n no signin paths enabled — set <code>paths.wallet</code> or{' '}\n <code>paths.email</code> to <code>true</code>\n </div>\n );\n }\n\n if (signedIn) {\n return (\n <div className={className} data-oc-signin=\"\">\n <LinkPromptStep\n method={signedIn.method}\n didOc={signedIn.didOc}\n authOrigin={authOrigin}\n onResolved={signedIn.proceed}\n />\n </div>\n );\n }\n\n const showWallet = walletEnabled && (path === 'wallet' || !emailEnabled);\n const showEmail = emailEnabled && (path === 'email' || !walletEnabled);\n const bothEnabled = walletEnabled && emailEnabled;\n // The method the user will actually sign in with — the active tab,\n // clamped when only one path is enabled. Drives the checkbox wording.\n const activeMethod: 'wallet' | 'email' = !emailEnabled\n ? 'wallet'\n : !walletEnabled\n ? 'email'\n : path;\n\n return (\n <div className={className} data-oc-signin=\"\">\n {oauthError && (\n <div\n data-oc-signin-oauth-error=\"\"\n style={{\n marginBottom: 14,\n padding: '0.6rem 0.75rem',\n border: '1px solid var(--destructive, #ef4444)',\n borderRadius: signinRadius(6),\n color: 'var(--destructive, #ef4444)',\n fontFamily: SIGNIN_FONT,\n fontSize: signinRoleSize('text', signinSize(11)),\n }}\n >\n That sign-in didn&apos;t finish. Try again, or use another way below.\n </div>\n )}\n {addMode && (\n <div\n data-oc-signin-add-mode=\"\"\n style={{\n marginBottom: 14,\n padding: '0.6rem 0.75rem',\n border: '1px solid var(--primary, #f97316)',\n borderLeftWidth: 3,\n borderRadius: signinRadius(4),\n background: 'color-mix(in srgb, var(--primary, #f97316) 8%, transparent)',\n color: 'var(--foreground, #fafafa)',\n fontFamily: SIGNIN_FONT,\n fontSize: signinRoleSize('text', signinSize(11)),\n lineHeight: 1.6,\n }}\n >\n <strong\n style={{\n color: 'var(--primary, #f97316)',\n letterSpacing: signinTracking('0.12em'),\n textTransform: SIGNIN_CASE,\n fontSize: signinSize(10),\n display: 'block',\n marginBottom: 3,\n }}\n >\n § adding another account\n </strong>\n Your current OrangeCheck account stays signed in. The new account joins your\n browser&apos;s roster; you can switch between them anytime from the account\n menu.\n </div>\n )}\n {/* Providers-first (ochk.io homepage): the familiar Google /\n GitHub on-ramp leads, with the wallet/email panel below the\n divider. Everywhere else this block stays at the bottom. */}\n {providersFirst && (\n <ProviderSignIn\n authOrigin={authOrigin}\n returnTo={resolvedReturn}\n add={addMode}\n credentials={credentials}\n firstParty={firstParty}\n providerHref={providerHref}\n first\n />\n )}\n {bothEnabled && (\n <div\n role=\"tablist\"\n aria-label=\"signin method\"\n data-oc-signin-tabs=\"\"\n style={{\n display: 'flex',\n gap: 8,\n marginBottom: 16,\n borderBottom: '1px solid var(--border, #27272a)',\n }}\n >\n {/* When providers lead, the easy email path reads first\n and the wallet path sits beside it as the upgrade. */}\n {providersFirst ? (\n <>\n <SigninTab\n active={path === 'email'}\n onClick={() => setPath('email')}\n >\n Email\n </SigninTab>\n <SigninTab\n active={path === 'wallet'}\n onClick={() => setPath('wallet')}\n >\n Bitcoin wallet\n </SigninTab>\n </>\n ) : (\n <>\n <SigninTab\n active={path === 'wallet'}\n onClick={() => setPath('wallet')}\n >\n Bitcoin wallet\n </SigninTab>\n <SigninTab\n active={path === 'email'}\n onClick={() => setPath('email')}\n >\n Email\n </SigninTab>\n </>\n )}\n </div>\n )}\n\n <div data-oc-signin-panel=\"\" role=\"tabpanel\">\n {showWallet && (\n <WalletFlow\n authOrigin={authOrigin}\n audience={audience}\n add={addMode}\n credentials={credentials}\n onSuccess={(a, t) => void handleSuccess(a, t, 'wallet')}\n />\n )}\n {showEmail && (\n <EmailFlow\n authOrigin={authOrigin}\n add={addMode}\n credentials={credentials}\n onSuccess={(a, t) => void handleSuccess(a, t, 'email')}\n />\n )}\n </div>\n\n {!providersFirst && (\n <ProviderSignIn\n authOrigin={authOrigin}\n returnTo={resolvedReturn}\n add={addMode}\n credentials={credentials}\n firstParty={firstParty}\n providerHref={providerHref}\n />\n )}\n\n {linkPrompt && (\n <label data-oc-signin-linkalso=\"\" style={linkAlsoStyle}>\n <input\n type=\"checkbox\"\n checked={linkAlso}\n onChange={(e) => setLinkAlso(e.target.checked)}\n style={{ margin: 0, accentColor: 'var(--primary, #f97316)' }}\n />\n <span>\n After signing in, also link{' '}\n {activeMethod === 'email' ? 'a Bitcoin wallet' : 'an email'}{' '}\n <span>— optional, one more signature.</span>\n </span>\n </label>\n )}\n </div>\n );\n}\n\n/* --- tab --- */\n\nfunction SigninTab({\n active,\n onClick,\n children,\n}: {\n active: boolean;\n onClick: () => void;\n children: React.ReactNode;\n}): React.ReactElement {\n return (\n <button\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={onClick}\n data-oc-signin-tab={active ? 'active' : 'inactive'}\n style={{\n minHeight: 44,\n padding: '0.6rem 0.875rem',\n background: 'transparent',\n border: 'none',\n borderBottom: active\n ? '2px solid var(--primary, #f97316)'\n : '2px solid transparent',\n color: active\n ? 'var(--foreground, #fafafa)'\n : 'var(--muted-foreground, #a1a1aa)',\n fontFamily: SIGNIN_FONT,\n fontSize: signinRoleSize('label', signinSize(11)),\n letterSpacing: signinTracking('0.12em'),\n textTransform: SIGNIN_CASE,\n cursor: 'pointer',\n marginBottom: -1,\n }}\n >\n {children}\n </button>\n );\n}\n\n/* --- third-party provider sign-in --- */\n\ninterface OAuthProviderEntry {\n id: string;\n label: string;\n}\n\n/**\n * Brand glyph for a known OAuth provider — rendered inside the\n * provider button so each entry is visually recognisable, but drawn\n * in `currentColor` so it picks up the OC muted-foreground theme\n * (the family palette wins over each vendor's brand colours by\n * design). Unknown providers fall through to no icon — the button\n * still renders, just text-only, so the registry is forward-compatible.\n */\nfunction ProviderIcon({ id }: { id: string }): React.ReactElement | null {\n const common = {\n width: 14,\n height: 14,\n viewBox: '0 0 24 24',\n fill: 'currentColor',\n 'aria-hidden': true,\n style: { flex: '0 0 auto' } as React.CSSProperties,\n } as const;\n if (id === 'google') {\n return (\n <svg {...common}>\n <path d=\"M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z\" />\n </svg>\n );\n }\n if (id === 'github') {\n return (\n <svg {...common}>\n <path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\" />\n </svg>\n );\n }\n return null;\n}\n\n/**\n * The secondary sign-in block — rendered below BIP-322 and email-OTP,\n * visibly less prominent. Each button is a plain navigation to the\n * auth host, which runs the OAuth dance and mints the family\n * `.ochk.io` session.\n *\n * The provider list is fetched from the auth host's\n * `/api/auth/providers` — a button appears only once that provider's\n * credentials are configured host-side. So enabling GitHub / Apple\n * later is a host env change, not a redeploy of every consumer site.\n * With no providers configured the whole block renders nothing.\n */\nfunction ProviderSignIn({\n authOrigin,\n returnTo,\n add,\n credentials,\n firstParty,\n providerHref,\n first = false,\n}: {\n authOrigin: string;\n returnTo: string;\n add: boolean;\n credentials: RequestCredentials;\n /** See OcSignInProps.mode: buttons only where the site starts them. */\n firstParty: boolean;\n providerHref?: (providerId: string) => string;\n /**\n * Render this block at the TOP of the ceremony (above the wallet/email\n * panel) rather than the default bottom. The \"or\" divider then sits\n * below the provider buttons, and the block reserves space beneath\n * itself instead of above. Set by {@link OcSignIn} when\n * `providersFirst` is on.\n */\n first?: boolean;\n}): React.ReactElement | null {\n // null while the list loads; [] once the host says none are configured.\n const [providers, setProviders] = React.useState<OAuthProviderEntry[] | null>(null);\n // A provider sign-in redirects THROUGH the auth host, so its final\n // redirect must carry an ABSOLUTE return target — a bare path would\n // resolve against ochk.io and strand a subdomain user there. The\n // origin is only knowable client-side.\n const [origin, setOrigin] = React.useState('');\n // A first-party site with no start route of its own has no provider path.\n const hidden = firstParty && !providerHref;\n\n React.useEffect(() => {\n setOrigin(window.location.origin);\n if (hidden) return;\n let cancelled = false;\n fetch(`${authOrigin}/api/auth/providers`, { credentials })\n .then((r) => (r.ok ? (r.json() as Promise<{ providers?: OAuthProviderEntry[] }>) : null))\n .then((body) => {\n if (!cancelled) setProviders(body?.providers ?? []);\n })\n .catch(() => {\n // auth host unreachable — no provider buttons; the\n // BIP-322 / email-OTP paths are unaffected.\n if (!cancelled) setProviders([]);\n });\n return () => {\n cancelled = true;\n };\n }, [authOrigin, credentials, hidden]);\n\n if (hidden) return null;\n if (providers !== null && providers.length === 0) return null;\n\n // An absolute family URL (the add-another-account round trip back to\n // a consumer subdomain) is carried verbatim through the OAuth hop;\n // a relative path is anchored to THIS page's origin.\n // It rides in the provider's start URL, so never with its fragment.\n const providerReturnTo = stripFragment(\n returnTo.startsWith('/') ? (origin ? `${origin}${returnTo}` : returnTo) : returnTo\n );\n const line = { flex: 1, height: 1, background: 'var(--border, #27272a)' } as const;\n // Vertical rhythm. The \"or\" divider is a horizontal rule between two\n // blocks, so it must sit EQUIDISTANT from both — otherwise it reads as\n // belonging to whichever block it is closer to. It previously ran\n // 14px above / 4px below (plus 4px of container margin), which glued the\n // rule to the panel beneath it and left the provider buttons floating.\n // BLOCK_GAP matches the tab row's own 16px marginBottom so the whole\n // ceremony sits on one 4px-scale rhythm rather than an ad-hoc\n // 0/4/8/14/16/20 mix.\n const BLOCK_GAP = 16;\n const BUTTON_GAP = 8; // siblings group tighter than blocks separate\n const divider = (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 10,\n // Symmetric in both orderings: in `first` mode the divider\n // owns the gap on both sides; at the bottom the container's\n // marginTop owns the gap above, so the divider only pads below.\n margin: first ? `${BLOCK_GAP}px 0` : `0 0 ${BLOCK_GAP}px`,\n color: 'var(--muted-foreground, #a1a1aa)',\n fontFamily: SIGNIN_FONT,\n fontSize: signinRoleSize('hint', signinSize(10)),\n letterSpacing: signinTracking('0.16em'),\n textTransform: SIGNIN_CASE,\n }}\n >\n <span style={line} />\n or\n <span style={line} />\n </div>\n );\n const buttonStyle = (i: number): React.CSSProperties => ({\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n gap: 10,\n boxSizing: 'border-box',\n width: '100%',\n minHeight: 44,\n marginTop: i === 0 ? 0 : BUTTON_GAP,\n padding: '0.6rem 0.875rem',\n border: '1px solid var(--border, #27272a)',\n borderRadius: SIGNIN_BUTTON_RADIUS,\n background: 'transparent',\n color: 'var(--muted-foreground, #a1a1aa)',\n fontFamily: SIGNIN_FONT,\n fontSize: signinSize(12),\n textDecoration: 'none',\n });\n // While the list loads, hold the space two buttons will take. Rendering\n // nothing and then inserting them above the form pushed it down after\n // first paint: most of /signin?add=1's CLS (0.11 at 390).\n if (providers === null) {\n if (!first) return null;\n return (\n <div data-oc-signin-providers=\"\" aria-hidden=\"true\">\n {[0, 1].map((i) => (\n <div key={i} style={{ ...buttonStyle(i), visibility: 'hidden' }}>\n &nbsp;\n </div>\n ))}\n {divider}\n </div>\n );\n }\n const buttons = providers.map((p, i) => (\n <a\n key={p.id}\n href={\n providerHref\n ? providerHref(p.id)\n : `${authOrigin}/api/auth/${p.id}/start?return_to=${encodeURIComponent(\n providerReturnTo\n )}${add ? '&add=1' : ''}`\n }\n data-oc-signin-provider={p.id}\n style={buttonStyle(i)}\n >\n <ProviderIcon id={p.id} />\n <span>{p.label}</span>\n </a>\n ));\n // first → providers lead: buttons, then the \"or\" divider before the\n // wallet/email panel below. Otherwise the canonical bottom placement:\n // divider, then buttons.\n return (\n <div\n data-oc-signin-providers=\"\"\n style={first ? { marginBottom: 0 } : { marginTop: BLOCK_GAP }}\n >\n {first ? (\n <>\n {buttons}\n {divider}\n </>\n ) : (\n <>\n {divider}\n {buttons}\n </>\n )}\n </div>\n );\n}\n\n/* --- wallet flow --- */\n\ninterface FlowProps {\n authOrigin: string;\n /** Multi-account add-mode · see OcSignIn.add. */\n add?: boolean;\n /** `omit` in first-party mode · see OcSignInProps.mode. */\n credentials: RequestCredentials;\n onSuccess: (account: OcAccount, token?: string) => void;\n}\n\ninterface WalletFlowProps extends FlowProps {\n audience: string;\n}\n\ntype AdapterShape = {\n detectWallets: () => Array<{ id: string; name: string; detected: boolean }>;\n getSigner: (id: string, opts: { address: string }) => (message: string) => Promise<string>;\n};\n\nasync function loadAdapter(): Promise<AdapterShape> {\n // A dependency, not an optional peer: this is a *static* dynamic import, so\n // the consumer's bundler must resolve it at build time and a missing package\n // fails the build. The old `Function('m','return import(m)')` trick produced\n // a runtime bare-specifier `import()` the browser cannot resolve without an\n // import map — which is what broke sign-in everywhere.\n return (await import('@orangecheck/wallet-adapter')) as unknown as AdapterShape;\n}\n\ntype DetectedWallet = { id: string; name: string };\n\nasync function detectBrowserWallets(): Promise<DetectedWallet[]> {\n return (await loadAdapter())\n .detectWallets()\n .filter((w) => w.detected && w.id !== 'manual')\n .map((w) => ({ id: w.id, name: w.name }));\n}\n\nfunction WalletFlow({\n authOrigin,\n audience,\n add,\n credentials,\n onSuccess,\n}: WalletFlowProps): React.ReactElement {\n const uid = React.useId();\n const ids = {\n address: `${uid}-address`,\n wallet: `${uid}-wallet`,\n message: `${uid}-message`,\n signature: `${uid}-signature`,\n };\n const [address, setAddress] = React.useState('');\n const [error, setError] = React.useState<string | null>(null);\n const [submitting, setSubmitting] = React.useState(false);\n // Browser wallets on this device; null until the adapter has loaded.\n const [wallets, setWallets] = React.useState<DetectedWallet[] | null>(null);\n const [walletId, setWalletId] = React.useState<string | null>(null);\n // No browser wallet (every phone, Sparrow, hardware wallets): show the\n // challenge to sign elsewhere and take the signature back. This used to\n // end in \"no BIP-322 wallet extension detected · install one and refresh\".\n const [manual, setManual] = React.useState<{ message: string; nonce: string } | null>(null);\n const [pasted, setPasted] = React.useState('');\n const [copied, setCopied] = React.useState<'idle' | 'copied' | 'failed'>('idle');\n\n React.useEffect(() => {\n let cancelled = false;\n detectBrowserWallets()\n .then((found) => {\n if (cancelled) return;\n setWallets(found);\n setWalletId(found[0]?.id ?? null);\n })\n .catch(() => {\n if (!cancelled) setWallets([]);\n });\n return () => {\n cancelled = true;\n };\n }, []);\n\n async function fetchChallenge(addr: string): Promise<{ message: string; nonce: string }> {\n const res = await fetch(\n `${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&audience=${encodeURIComponent(audience)}&purpose=login`\n );\n const json = (await res.json()) as { message?: string; nonce?: string; error?: string };\n if (!res.ok || !json.message || !json.nonce) {\n throw new Error(json.error ?? 'challenge_failed');\n }\n return { message: json.message, nonce: json.nonce };\n }\n\n /**\n * POST the signature. A browser wallet signs BIP-322, so that path names\n * the scheme. A pasted signature may be legacy BIP-137 (Electrum, most\n * hardware wallets on a `1…` address), which the verifier only detects\n * when no scheme is named, so the manual path leaves it out.\n */\n async function submitSignature(\n message: string,\n nonce: string,\n signature: string,\n scheme: 'bip322' | undefined\n ): Promise<void> {\n // Family-CORS lets the .ochk.io session cookie land cross-origin.\n const res = await fetch(`${authOrigin}/api/auth/signin`, {\n method: 'POST',\n credentials,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n message,\n signature,\n ...(scheme ? { scheme } : {}),\n expectedNonce: nonce,\n expectedAudience: audience,\n expectedPurpose: 'login',\n // Multi-account add-mode · the auth host preserves the current\n // roster_id when set, instead of minting a fresh one.\n ...(add ? { add: true } : {}),\n }),\n });\n const json = (await res.json()) as SigninJson;\n if (!res.ok || !('ok' in json) || !json.ok || !json.account) {\n throw new Error(\n ('reason' in json && json.reason) ||\n ('error' in json && json.error) ||\n 'verify_failed'\n );\n }\n onSuccess(json.account, json.token);\n }\n\n async function signIn(e: React.FormEvent): Promise<void> {\n e.preventDefault();\n const addr = address.trim();\n if (!addr) {\n setError('Paste your Bitcoin address.');\n return;\n }\n if (!looksLikeBitcoinAddress(addr)) {\n setError(humanSigninError('bad_request', 'wallet'));\n return;\n }\n setError(null);\n setSubmitting(true);\n let step: 'challenge' | 'submit' = 'challenge';\n try {\n // Submitting before detection finished must not send someone with\n // an extension to the paste-a-signature path.\n let id = walletId;\n if (wallets === null) {\n const found = await detectBrowserWallets().catch(() => []);\n setWallets(found);\n id = found[0]?.id ?? null;\n setWalletId(id);\n }\n const challenge = await fetchChallenge(addr);\n if (!id) {\n setManual(challenge);\n setSubmitting(false);\n return;\n }\n const signer = (await loadAdapter()).getSigner(id, { address: addr });\n const signature = await signer(challenge.message);\n step = 'submit';\n await submitSignature(challenge.message, challenge.nonce, signature, 'bip322');\n } catch (err) {\n setError(failureMessage(err, 'wallet', step));\n setSubmitting(false);\n }\n }\n\n async function signInManual(e: React.FormEvent): Promise<void> {\n e.preventDefault();\n if (!manual) return;\n const sig = pasted.trim();\n if (!sig) {\n setError('Paste the signature your wallet gave you.');\n return;\n }\n setError(null);\n setSubmitting(true);\n try {\n await submitSignature(manual.message, manual.nonce, sig, undefined);\n } catch (err) {\n setError(failureMessage(err, 'wallet', 'submit'));\n setSubmitting(false);\n }\n }\n\n async function copyMessage(): Promise<void> {\n if (!manual) return;\n try {\n await navigator.clipboard.writeText(manual.message);\n setCopied('copied');\n } catch {\n setCopied('failed');\n }\n }\n\n if (manual) {\n return (\n <form onSubmit={signInManual} data-oc-signin-wallet-manual=\"\">\n <FlowHeader>\n In your wallet, use “sign message” (BIP-322) with {address.trim()}, sign the\n text below, and paste the signature back here. Nothing is spent.\n </FlowHeader>\n <Label htmlFor={ids.message}>Message to sign</Label>\n <textarea\n id={ids.message}\n readOnly\n value={manual.message}\n rows={5}\n onFocus={(e) => e.currentTarget.select()}\n style={{ ...inputStyle, resize: 'vertical' }}\n />\n <button type=\"button\" onClick={() => void copyMessage()} style={linkButtonStyle}>\n {copied === 'copied' ? 'Copied' : 'Copy message'}\n </button>\n {copied === 'failed' && (\n <Hint>Couldn&apos;t copy here. Select the text above and copy it.</Hint>\n )}\n <div style={{ marginTop: 14 }}>\n <Label htmlFor={ids.signature}>Signature</Label>\n <textarea\n id={ids.signature}\n value={pasted}\n onChange={(e) => {\n setPasted(e.target.value);\n if (error) setError(null);\n }}\n rows={3}\n placeholder=\"paste the signature\"\n spellCheck={false}\n autoFocus\n aria-invalid={error ? true : undefined}\n style={{ ...inputStyle, resize: 'vertical' }}\n />\n </div>\n {error && <ErrorLine>{error}</ErrorLine>}\n <button\n type=\"submit\"\n disabled={submitting || !pasted.trim()}\n style={submitStyle(submitting || !pasted.trim())}\n >\n {submitting ? 'Checking…' : <Submit>Sign me in</Submit>}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setManual(null);\n setPasted('');\n setError(null);\n setCopied('idle');\n }}\n style={linkButtonStyle}\n >\n Use a different address\n </button>\n </form>\n );\n }\n\n const noBrowserWallet = wallets !== null && wallets.length === 0;\n return (\n <form onSubmit={signIn} data-oc-signin-wallet=\"\">\n <FlowHeader>\n Your wallet signs a short message to prove you hold the address. Nothing is spent\n and your key never leaves the wallet. Works with browser wallets, or any wallet that\n can sign a message, like Sparrow.\n </FlowHeader>\n <Label htmlFor={ids.address}>Bitcoin address</Label>\n <input\n id={ids.address}\n type=\"text\"\n value={address}\n onChange={(e) => {\n setAddress(e.target.value);\n if (error) setError(null);\n }}\n placeholder=\"bc1q… · 3… · 1…\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n spellCheck={false}\n required\n aria-invalid={error ? true : undefined}\n style={inputStyle}\n />\n {wallets && wallets.length > 1 && (\n <div style={{ marginTop: 12 }}>\n <Label htmlFor={ids.wallet}>Sign with</Label>\n <select\n id={ids.wallet}\n value={walletId ?? ''}\n onChange={(e) => setWalletId(e.target.value)}\n style={inputStyle}\n >\n {wallets.map((w) => (\n <option key={w.id} value={w.id}>\n {w.name}\n </option>\n ))}\n </select>\n </div>\n )}\n {error && <ErrorLine>{error}</ErrorLine>}\n <button\n type=\"submit\"\n disabled={submitting || !address.trim()}\n style={submitStyle(submitting || !address.trim())}\n >\n {submitting\n ? 'Waiting for wallet…'\n : noBrowserWallet\n ? <Submit>Get the message to sign</Submit>\n : <Submit>Sign in with wallet</Submit>}\n </button>\n {noBrowserWallet && (\n <Hint>No browser wallet found here. You can sign with any wallet and paste.</Hint>\n )}\n </form>\n );\n}\n\n/* --- email flow --- */\n\ntype EmailStage = 'enter' | 'code';\n\nfunction EmailFlow({ authOrigin, add, credentials, onSuccess }: FlowProps): React.ReactElement {\n const uid = React.useId();\n const [stage, setStage] = React.useState<EmailStage>('enter');\n const [email, setEmail] = React.useState('');\n const [emailError, setEmailError] = React.useState<string | null>(null);\n const [code, setCode] = React.useState('');\n const [codeError, setCodeError] = React.useState<string | null>(null);\n const [token, setToken] = React.useState<string | null>(null);\n const [submitting, setSubmitting] = React.useState(false);\n const [resent, setResent] = React.useState(false);\n\n /** Ask the host to email a code. Returns a human error, or null on success. */\n async function sendCode(address: string): Promise<string | null> {\n try {\n const res = await fetch(`${authOrigin}/api/auth/email-otp/start`, {\n method: 'POST',\n credentials,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: address }),\n });\n const json = (await res.json()) as { ok?: boolean; token?: string; reason?: string };\n if (!res.ok || !json.token) return humanSigninError(json.reason ?? 'server_error', 'email');\n setToken(json.token);\n return null;\n } catch {\n return NETWORK_ERROR;\n }\n }\n\n async function start(e: React.FormEvent): Promise<void> {\n e.preventDefault();\n const trimmed = email.trim().toLowerCase();\n if (!trimmed || !/.+@.+\\..+/.test(trimmed)) {\n setEmailError(humanSigninError('invalid_email', 'email'));\n return;\n }\n setEmailError(null);\n setSubmitting(true);\n const err = await sendCode(trimmed);\n setSubmitting(false);\n if (err) {\n setEmailError(err);\n return;\n }\n setEmail(trimmed);\n setStage('code');\n }\n\n async function resend(): Promise<void> {\n setCodeError(null);\n setResent(false);\n setSubmitting(true);\n const err = await sendCode(email);\n setSubmitting(false);\n setCode('');\n if (err) setCodeError(err);\n else setResent(true);\n }\n\n async function verify(e: React.FormEvent): Promise<void> {\n e.preventDefault();\n if (!token) return;\n if (code.length !== 6) {\n setCodeError('The code is 6 digits.');\n return;\n }\n setCodeError(null);\n setSubmitting(true);\n try {\n const res = await fetch(`${authOrigin}/api/auth/email-otp/verify`, {\n method: 'POST',\n credentials,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n email,\n code,\n token,\n // Multi-account add-mode · see WalletFlow.\n ...(add ? { add: true } : {}),\n }),\n });\n const json = (await res.json()) as SigninJson;\n if (!res.ok || !('ok' in json) || !json.ok || !json.account) {\n throw new Error(\n ('reason' in json && json.reason) ||\n ('error' in json && json.error) ||\n 'server_error'\n );\n }\n onSuccess(json.account, json.token);\n } catch (err) {\n setCodeError(failureMessage(err, 'email', 'submit'));\n } finally {\n setSubmitting(false);\n }\n }\n\n if (stage === 'enter') {\n return (\n <form onSubmit={start} data-oc-signin-email=\"\">\n <FlowHeader>\n We email you a 6-digit code. No password, and no wallet needed to start. Add a\n Bitcoin address any time: a way in that only you hold.\n </FlowHeader>\n <Label htmlFor={`${uid}-email`}>Email</Label>\n <input\n id={`${uid}-email`}\n type=\"email\"\n autoComplete=\"email\"\n value={email}\n onChange={(e) => {\n setEmail(e.target.value);\n if (emailError) setEmailError(null);\n }}\n placeholder=\"you@example.com\"\n required\n aria-invalid={emailError ? true : undefined}\n style={inputStyle}\n />\n {emailError && <ErrorLine>{emailError}</ErrorLine>}\n <button type=\"submit\" disabled={submitting} style={submitStyle(submitting)}>\n {submitting ? 'Sending…' : <Submit>Send one-time code</Submit>}\n </button>\n </form>\n );\n }\n\n return (\n <form onSubmit={verify} data-oc-signin-email-verify=\"\">\n <FlowHeader>\n {resent ? 'Sent a new code to ' : 'Sent a 6-digit code to '}\n <span style={{ color: 'var(--foreground, #fafafa)' }}>{email}</span>. It expires in\n 10 minutes.\n </FlowHeader>\n <Label htmlFor={`${uid}-code`}>One-time code</Label>\n <input\n id={`${uid}-code`}\n type=\"text\"\n inputMode=\"numeric\"\n pattern=\"\\d{6}\"\n maxLength={6}\n autoComplete=\"one-time-code\"\n value={code}\n onChange={(e) => {\n setCode(e.target.value.replace(/\\D/g, '').slice(0, 6));\n if (codeError) setCodeError(null);\n }}\n placeholder=\"6 digits\"\n required\n autoFocus\n aria-invalid={codeError ? true : undefined}\n style={{ ...inputStyle, letterSpacing: '0.4em', fontSize: 16 }}\n />\n {codeError && <ErrorLine>{codeError}</ErrorLine>}\n <button\n type=\"submit\"\n disabled={submitting || code.length !== 6}\n style={submitStyle(submitting || code.length !== 6)}\n >\n {submitting ? 'Verifying…' : <Submit>Verify · sign me in</Submit>}\n </button>\n <div>\n <button\n type=\"button\"\n onClick={() => void resend()}\n disabled={submitting}\n style={linkButtonStyle}\n >\n Send a new code\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setStage('enter');\n setCode('');\n setToken(null);\n setResent(false);\n }}\n style={linkButtonStyle}\n >\n Use a different email\n </button>\n </div>\n </form>\n );\n}\n\n/* --- shared subcomponents --- */\n\n/** A submit label and its arrow, which a site can hide (--oc-signin-arrow). */\nfunction Submit({ children }: { children: string }): React.ReactElement {\n return (\n <>\n {children}\n <span aria-hidden=\"true\" style={{ display: SIGNIN_ARROW }}>\n {' →'}\n </span>\n </>\n );\n}\n\nfunction FlowHeader({\n label,\n children,\n}: {\n /** Omitted where the active tab already names the method. */\n label?: string;\n children: React.ReactNode;\n}): React.ReactElement {\n return (\n <div style={{ marginBottom: 16 }}>\n {label && (\n <div\n style={{\n color: 'var(--primary, #f97316)',\n fontFamily: SIGNIN_FONT,\n fontSize: signinSize(10),\n letterSpacing: signinTracking('0.18em'),\n textTransform: SIGNIN_CASE,\n marginBottom: 6,\n }}\n >\n {label}\n </div>\n )}\n <p\n style={{\n color: 'var(--muted-foreground, #a1a1aa)',\n fontSize: signinRoleSize('text', '13px'),\n lineHeight: 1.55,\n margin: 0,\n }}\n >\n {children}\n </p>\n </div>\n );\n}\n\nconst linkButtonStyle: React.CSSProperties = {\n display: 'inline-flex',\n alignItems: 'center',\n minHeight: 44,\n marginTop: 4,\n marginRight: 16,\n background: 'transparent',\n border: 'none',\n color: 'var(--muted-foreground, #a1a1aa)',\n fontFamily: SIGNIN_FONT,\n fontSize: signinRoleSize('button', signinSize(11)),\n letterSpacing: signinTracking('0.12em'),\n textTransform: SIGNIN_CASE,\n cursor: 'pointer',\n padding: 0,\n};\n\nfunction Label({\n htmlFor,\n children,\n}: {\n htmlFor: string;\n children: React.ReactNode;\n}): React.ReactElement {\n return (\n <label\n htmlFor={htmlFor}\n style={{\n display: 'block',\n color: 'var(--foreground, #fafafa)',\n fontFamily: SIGNIN_FONT,\n fontSize: signinRoleSize('label', signinSize(11)),\n letterSpacing: signinTracking('0.12em'),\n textTransform: SIGNIN_CASE,\n marginBottom: 6,\n }}\n >\n {children}\n </label>\n );\n}\n\nconst inputStyle: React.CSSProperties = {\n width: '100%',\n padding: '0.55rem 0.75rem',\n background: 'var(--background, #0a0a0a)',\n color: 'var(--foreground, #fafafa)',\n border: '1px solid var(--input, #27272a)',\n borderRadius: signinRadius(6),\n fontFamily: SIGNIN_FONT,\n // 16px — never below it. A sub-16px font-size makes iOS Safari\n // auto-zoom the viewport on focus; the family rule is >=16px on\n // every form field. Desktop reads fine at 16 too.\n fontSize: 16,\n outline: 'none',\n};\n\nconst linkAlsoStyle: React.CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n // The whole row is the checkbox's hit area; the 13px box alone was not.\n minHeight: 44,\n gap: 8,\n marginTop: 16,\n paddingTop: 14,\n borderTop: '1px solid var(--border, #27272a)',\n color: 'var(--muted-foreground, #a1a1aa)',\n fontFamily: SIGNIN_FONT,\n fontSize: signinRoleSize('text', signinSize(12)),\n lineHeight: 1.5,\n cursor: 'pointer',\n};\n\nfunction submitStyle(disabled: boolean): React.CSSProperties {\n return {\n marginTop: 12,\n padding: '0.6rem 1rem',\n background: 'var(--primary, #f97316)',\n color: 'var(--primary-foreground, #0a0a0a)',\n border: 'none',\n borderRadius: SIGNIN_BUTTON_RADIUS,\n fontFamily: SIGNIN_FONT,\n fontSize: signinRoleSize('button', signinSize(11)),\n letterSpacing: signinTracking('0.14em'),\n textTransform: SIGNIN_CASE,\n fontWeight: SIGNIN_STRONG,\n cursor: disabled ? 'not-allowed' : 'pointer',\n opacity: disabled ? 0.5 : 1,\n width: '100%',\n };\n}\n\nfunction ErrorLine({ children }: { children: React.ReactNode }): React.ReactElement {\n return (\n <p\n role=\"alert\"\n style={{\n marginTop: 6,\n marginBottom: 0,\n color: 'var(--destructive, #ef4444)',\n fontFamily: SIGNIN_FONT,\n fontSize: signinRoleSize('hint', signinSize(11)),\n letterSpacing: '0.04em',\n }}\n >\n {children}\n </p>\n );\n}\n\nfunction Hint({ children }: { children: React.ReactNode }): React.ReactElement {\n return (\n <p\n style={{\n marginTop: 12,\n marginBottom: 0,\n color: 'var(--muted-foreground, #a1a1aa)',\n fontFamily: SIGNIN_FONT,\n fontSize: signinRoleSize('hint', '10.5px'),\n lineHeight: 1.55,\n }}\n >\n {children}\n </p>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,187 @@
1
+ // src/tab-session.ts
2
+ var TAB_SESSION_HEADER = "x-oc-tab-session";
3
+ var TAB_SESSION_STORAGE_KEY = "oc_tab_session";
4
+ var TAB_ACCOUNT_HINT_KEY = "oc-as";
5
+ var TAB_ADOPT_HASH = "#oc-adopt";
6
+ function readTabSession() {
7
+ if (typeof window === "undefined") return null;
8
+ try {
9
+ const raw = window.sessionStorage.getItem(TAB_SESSION_STORAGE_KEY);
10
+ if (!raw) return null;
11
+ const parsed = JSON.parse(raw);
12
+ if (typeof parsed.token !== "string" || parsed.token.length === 0) return null;
13
+ if (typeof parsed.didOc !== "string" || parsed.didOc.length === 0) return null;
14
+ return { token: parsed.token, didOc: parsed.didOc };
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ function writeTabSession(session) {
20
+ if (typeof window === "undefined") return;
21
+ try {
22
+ window.sessionStorage.setItem(TAB_SESSION_STORAGE_KEY, JSON.stringify(session));
23
+ } catch {
24
+ }
25
+ }
26
+ function clearTabSession() {
27
+ if (typeof window === "undefined") return;
28
+ try {
29
+ window.sessionStorage.removeItem(TAB_SESSION_STORAGE_KEY);
30
+ } catch {
31
+ }
32
+ }
33
+ function tabSessionHeader() {
34
+ const pin = readTabSession();
35
+ return pin ? { [TAB_SESSION_HEADER]: pin.token } : {};
36
+ }
37
+ function isPinnableUrl(url, authOrigin) {
38
+ if (typeof window === "undefined") return false;
39
+ try {
40
+ const u = new URL(url, window.location.href);
41
+ return u.origin === window.location.origin || u.origin === authOrigin;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+ function installTabFetchInterceptor(authOrigin) {
47
+ if (typeof window === "undefined") return () => {
48
+ };
49
+ const original = window.fetch;
50
+ const wrapped = (input, init) => {
51
+ try {
52
+ const pin = readTabSession();
53
+ if (pin) {
54
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
55
+ if (isPinnableUrl(url, authOrigin)) {
56
+ const headers = new Headers(
57
+ init?.headers ?? (input instanceof Request ? input.headers : void 0)
58
+ );
59
+ if (!headers.has("authorization") && !headers.has(TAB_SESSION_HEADER)) {
60
+ headers.set(TAB_SESSION_HEADER, pin.token);
61
+ init = { ...init, headers };
62
+ }
63
+ }
64
+ }
65
+ } catch {
66
+ }
67
+ return original.call(window, input, init);
68
+ };
69
+ window.fetch = wrapped;
70
+ return () => {
71
+ if (window.fetch === wrapped) window.fetch = original;
72
+ };
73
+ }
74
+ function consumeTabAdoptMarker() {
75
+ if (typeof window === "undefined") return false;
76
+ if (!window.location.hash.includes(TAB_ADOPT_HASH.slice(1))) return false;
77
+ clearTabSession();
78
+ try {
79
+ const url = new URL(window.location.href);
80
+ url.hash = "";
81
+ window.history.replaceState(window.history.state, "", url.toString());
82
+ } catch {
83
+ }
84
+ return true;
85
+ }
86
+ var OC_AS_HINT_RE = /^oc-as=(did:oc:[0-9a-f]{32})$/;
87
+ function familyApex(authOrigin) {
88
+ try {
89
+ return new URL(authOrigin).hostname.toLowerCase();
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+ function isFamilyUrl(u, authOrigin) {
95
+ if (u.protocol !== "http:" && u.protocol !== "https:") return false;
96
+ if (typeof window !== "undefined" && u.origin === window.location.origin) return true;
97
+ const apex = familyApex(authOrigin);
98
+ if (!apex) return false;
99
+ const host = u.hostname.toLowerCase();
100
+ return host === apex || host.endsWith(`.${apex}`);
101
+ }
102
+ function opensWithoutTabPin(e, anchor, dest) {
103
+ if (typeof window !== "undefined" && dest.origin !== window.location.origin) return true;
104
+ const target = (anchor.getAttribute("target") ?? "").toLowerCase();
105
+ return e.button === 1 || // middle-click
106
+ e.metaKey || e.ctrlKey || e.shiftKey || target === "_blank";
107
+ }
108
+ function installTabLinkDecorator(authOrigin) {
109
+ if (typeof window === "undefined" || typeof document === "undefined") return () => {
110
+ };
111
+ const onActivate = (e) => {
112
+ try {
113
+ const pin = readTabSession();
114
+ if (!pin) return;
115
+ const target = e.target;
116
+ if (!(target instanceof Element)) return;
117
+ const anchor = target.closest("a[href]");
118
+ if (!anchor || anchor.hasAttribute("download")) return;
119
+ const raw = anchor.getAttribute("href");
120
+ if (!raw || raw.includes(`${TAB_ACCOUNT_HINT_KEY}=`)) return;
121
+ let dest;
122
+ try {
123
+ dest = new URL(anchor.href);
124
+ } catch {
125
+ return;
126
+ }
127
+ if (dest.hash) return;
128
+ if (!isFamilyUrl(dest, authOrigin)) return;
129
+ if (!opensWithoutTabPin(e, anchor, dest)) return;
130
+ anchor.setAttribute("href", `${raw}#${TAB_ACCOUNT_HINT_KEY}=${pin.didOc}`);
131
+ } catch {
132
+ }
133
+ };
134
+ window.addEventListener("click", onActivate, true);
135
+ window.addEventListener("auxclick", onActivate, true);
136
+ return () => {
137
+ window.removeEventListener("click", onActivate, true);
138
+ window.removeEventListener("auxclick", onActivate, true);
139
+ };
140
+ }
141
+ async function consumeTabAccountHint(authOrigin) {
142
+ if (typeof window === "undefined") return null;
143
+ const frag = window.location.hash.replace(/^#/, "");
144
+ const m = OC_AS_HINT_RE.exec(frag);
145
+ if (!m) return null;
146
+ const did = m[1];
147
+ try {
148
+ const url = new URL(window.location.href);
149
+ url.hash = "";
150
+ window.history.replaceState(window.history.state, "", url.toString());
151
+ } catch {
152
+ }
153
+ const existing = readTabSession();
154
+ if (existing && existing.didOc === did) return did;
155
+ try {
156
+ const res = await fetch(`${authOrigin}/api/auth/tab`, {
157
+ method: "POST",
158
+ credentials: "include",
159
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
160
+ body: JSON.stringify({ did_oc: did })
161
+ });
162
+ if (!res.ok) return null;
163
+ const body = await res.json();
164
+ if (body.ok && typeof body.token === "string" && body.account?.did_oc === did) {
165
+ writeTabSession({ token: body.token, didOc: did });
166
+ return did;
167
+ }
168
+ } catch {
169
+ }
170
+ return null;
171
+ }
172
+
173
+ export {
174
+ TAB_SESSION_HEADER,
175
+ TAB_SESSION_STORAGE_KEY,
176
+ TAB_ACCOUNT_HINT_KEY,
177
+ TAB_ADOPT_HASH,
178
+ readTabSession,
179
+ writeTabSession,
180
+ clearTabSession,
181
+ tabSessionHeader,
182
+ installTabFetchInterceptor,
183
+ consumeTabAdoptMarker,
184
+ installTabLinkDecorator,
185
+ consumeTabAccountHint
186
+ };
187
+ //# sourceMappingURL=chunk-TJJNTITX.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tab-session.ts"],"sourcesContent":["/**\n * Per-tab account pinning · sessionStorage-backed.\n *\n * One browser holds ONE shared `oc_session` cookie (the browser-wide\n * default account) but may be signed into N roster accounts at once. A\n * tab pins itself to one account by holding that account's session JWT\n * here — sessionStorage is per-tab by nature — and sending it on every\n * same-site API call as the `x-oc-tab-session` header. Servers verify\n * the header exactly like the cookie token (it IS a session JWT, a\n * credential, never a bare account selector), so switching accounts in\n * one tab no longer yanks the identity out from under every other tab.\n *\n * Trade-off, named: a pinned token is readable by same-origin script,\n * unlike the HttpOnly cookie. An XSS on a family site can already act\n * as the user via `credentials: 'include'`; the marginal exposure is\n * token exfiltration for offline use, bounded by the token's TTL and\n * the host's per-row revocation. The host has echoed session JWTs in\n * `/api/auth/signin` / `/api/auth/switch` response bodies since the\n * multi-account migration, so this channel is not new.\n */\n\n/** Header carrying the tab-pinned session JWT. Mirrors `@orangecheck/auth-core`'s `TAB_SESSION_HEADER`. */\nexport const TAB_SESSION_HEADER = 'x-oc-tab-session';\n\nexport const TAB_SESSION_STORAGE_KEY = 'oc_tab_session';\n\n/**\n * URL-fragment key the link decorator stamps onto family-origin\n * navigations so a NEW tab (or a cross-subdomain document GET) inherits\n * the OPENER tab's effective account instead of falling back to the\n * shared cookie's default account.\n *\n * The value is the account's `did:oc` — a PUBLIC identifier, never the\n * session JWT. It rides a fragment (`#oc-as=<did>`), which is never sent\n * to any server, and the host re-validates roster membership before\n * minting a tab token for it, so a hand-crafted link grants nothing the\n * visitor's own cookie/roster doesn't already authorize.\n */\nexport const TAB_ACCOUNT_HINT_KEY = 'oc-as';\n\n/**\n * Hash marker the auth host appends to a post-ceremony redirect so the\n * landing tab adopts the cookie account instead of keeping a stale pin\n * (the user just completed a sign-in/add ceremony and expects to BE the\n * new account in that tab). Never sent to any server (it's a fragment).\n */\nexport const TAB_ADOPT_HASH = '#oc-adopt';\n\nexport interface OcTabSession {\n token: string;\n didOc: string;\n}\n\nexport function readTabSession(): OcTabSession | null {\n if (typeof window === 'undefined') return null;\n try {\n const raw = window.sessionStorage.getItem(TAB_SESSION_STORAGE_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as { token?: unknown; didOc?: unknown };\n if (typeof parsed.token !== 'string' || parsed.token.length === 0) return null;\n if (typeof parsed.didOc !== 'string' || parsed.didOc.length === 0) return null;\n return { token: parsed.token, didOc: parsed.didOc };\n } catch {\n // sessionStorage can throw (privacy modes) · malformed JSON → unpinned\n return null;\n }\n}\n\nexport function writeTabSession(session: OcTabSession): void {\n if (typeof window === 'undefined') return;\n try {\n window.sessionStorage.setItem(TAB_SESSION_STORAGE_KEY, JSON.stringify(session));\n } catch {\n // quota / privacy mode — degrade to unpinned (cookie-default) behavior\n }\n}\n\nexport function clearTabSession(): void {\n if (typeof window === 'undefined') return;\n try {\n window.sessionStorage.removeItem(TAB_SESSION_STORAGE_KEY);\n } catch {\n // nothing to clear\n }\n}\n\n/** `{ 'x-oc-tab-session': <jwt> }` when this tab is pinned, else `{}`. */\nexport function tabSessionHeader(): Record<string, string> {\n const pin = readTabSession();\n return pin ? { [TAB_SESSION_HEADER]: pin.token } : {};\n}\n\n/**\n * Should this request carry the tab pin? Same-origin requests (relative\n * URLs or the page's own origin) and requests to the auth host — never\n * third parties.\n */\nfunction isPinnableUrl(url: string, authOrigin: string): boolean {\n if (typeof window === 'undefined') return false;\n try {\n const u = new URL(url, window.location.href);\n return u.origin === window.location.origin || u.origin === authOrigin;\n } catch {\n return false;\n }\n}\n\n/**\n * Install a scoped `window.fetch` wrapper that attaches the tab pin to\n * every same-site request, so app-level data fetches (vault shares, me\n * wallet, projects, …) execute as the account this tab displays —\n * without touching any call site. Returns an uninstaller.\n *\n * Conservative by design:\n * - no pin → pass-through (zero behavior change)\n * - only same-origin + auth-host URLs (see {@link isPinnableUrl})\n * - never overrides an existing `Authorization` or `x-oc-tab-session`\n * header (programmatic Bearer / integrator-token paths keep their\n * own credential semantics)\n * - any internal error → original fetch, untouched\n */\nexport function installTabFetchInterceptor(authOrigin: string): () => void {\n if (typeof window === 'undefined') return () => {};\n const original = window.fetch;\n const wrapped: typeof window.fetch = (input, init) => {\n try {\n const pin = readTabSession();\n if (pin) {\n const url =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.href\n : input.url;\n if (isPinnableUrl(url, authOrigin)) {\n const headers = new Headers(\n init?.headers ?? (input instanceof Request ? input.headers : undefined)\n );\n if (!headers.has('authorization') && !headers.has(TAB_SESSION_HEADER)) {\n headers.set(TAB_SESSION_HEADER, pin.token);\n init = { ...init, headers };\n }\n }\n }\n } catch {\n // never break a fetch over pinning\n }\n return original.call(window, input as RequestInfo | URL, init);\n };\n window.fetch = wrapped;\n return () => {\n // only restore if nobody else wrapped on top of us in the meantime\n if (window.fetch === wrapped) window.fetch = original;\n };\n}\n\n/**\n * If the URL carries the {@link TAB_ADOPT_HASH} marker, clear the pin\n * (so the tab adopts the cookie account) and strip the marker from the\n * address bar. Returns `true` when adoption happened.\n */\nexport function consumeTabAdoptMarker(): boolean {\n if (typeof window === 'undefined') return false;\n if (!window.location.hash.includes(TAB_ADOPT_HASH.slice(1))) return false;\n clearTabSession();\n try {\n const url = new URL(window.location.href);\n url.hash = '';\n window.history.replaceState(window.history.state, '', url.toString());\n } catch {\n // cosmetic only — a lingering #oc-adopt re-adopts on reload, which\n // is idempotent (the pin is already cleared).\n }\n return true;\n}\n\n// ─── Cross-tab account inheritance (the new-tab pin handoff) ────────────\n//\n// The pin lives in sessionStorage, which a NEW tab does not inherit\n// cross-origin (and not at all on Safari). So a CTRL/⌘/middle-click on a\n// family link — or a same-tab navigation to a sibling subdomain — would\n// land unpinned and resolve to the shared cookie's DEFAULT account, i.e.\n// \"the other account.\" We bridge the gap WITHOUT putting a credential in\n// a URL: the opener tab stamps the destination link with the public\n// `did:oc` of its effective account (`#oc-as=<did>`), and the landing\n// tab mints its own pin for that account via the host (which re-checks\n// roster membership). The did is a selector, not a credential.\n\nconst OC_AS_HINT_RE = /^oc-as=(did:oc:[0-9a-f]{32})$/;\n\n/** Apex hostname of the auth host, e.g. `ochk.io`. Null if unparseable. */\nfunction familyApex(authOrigin: string): string | null {\n try {\n return new URL(authOrigin).hostname.toLowerCase();\n } catch {\n return null;\n }\n}\n\n/**\n * Is `u` a family destination worth stamping — current origin or any\n * host at/under the auth host's apex (so `stamp.ochk.io`, `ochk.io`,\n * etc. match, but `example.com` never does)? http(s) only — `mailto:`,\n * `tel:`, `javascript:`, `blob:` are excluded by construction.\n */\nfunction isFamilyUrl(u: URL, authOrigin: string): boolean {\n if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;\n if (typeof window !== 'undefined' && u.origin === window.location.origin) return true;\n const apex = familyApex(authOrigin);\n if (!apex) return false;\n const host = u.hostname.toLowerCase();\n return host === apex || host.endsWith(`.${apex}`);\n}\n\n/** Would following this link land in a context that can't see this tab's pin? */\nfunction opensWithoutTabPin(e: MouseEvent, anchor: HTMLAnchorElement, dest: URL): boolean {\n // Cross-origin always loses the pin (sessionStorage is per-origin).\n if (typeof window !== 'undefined' && dest.origin !== window.location.origin) return true;\n // Same-origin: only a NEW tab/window risks losing it (Safari doesn't\n // clone sessionStorage); a same-tab navigation keeps it, so leave\n // those links untouched to avoid needless URL noise.\n const target = (anchor.getAttribute('target') ?? '').toLowerCase();\n return (\n e.button === 1 || // middle-click\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n target === '_blank'\n );\n}\n\n/**\n * Install a capture-phase listener that stamps the tab's effective\n * `did:oc` onto outgoing family-origin links the instant they're\n * activated, so a new/cross-subdomain tab adopts THIS tab's account.\n * Returns an uninstaller. Mirrors {@link installTabFetchInterceptor}'s\n * conservatism:\n * - no pin → never stamps (zero behavior change)\n * - family origins only → never leaks the did to third parties\n * - reads the pin FRESH per event (no install-time capture)\n * - never clobbers an existing `#fragment`, a download, or a\n * non-http(s) scheme\n * - any internal error → leaves the link alone\n *\n * It mutates `a.href` in place rather than calling `window.open`, so it\n * never trips a popup blocker and the browser's own ctrl/⌘/middle-click\n * handling (and the context menu's \"open in new tab\") all carry the\n * stamp. The stamp is idempotent and self-healing — React resets the\n * href on the next render, and the destination strips the fragment via\n * {@link consumeTabAccountHint}.\n */\nexport function installTabLinkDecorator(authOrigin: string): () => void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return () => {};\n const onActivate = (e: MouseEvent) => {\n try {\n const pin = readTabSession();\n if (!pin) return;\n const target = e.target;\n if (!(target instanceof Element)) return;\n const anchor = target.closest('a[href]') as HTMLAnchorElement | null;\n if (!anchor || anchor.hasAttribute('download')) return;\n const raw = anchor.getAttribute('href');\n if (!raw || raw.includes(`${TAB_ACCOUNT_HINT_KEY}=`)) return;\n let dest: URL;\n try {\n dest = new URL(anchor.href);\n } catch {\n return;\n }\n if (dest.hash) return; // don't clobber a real #anchor deep-link\n if (!isFamilyUrl(dest, authOrigin)) return;\n if (!opensWithoutTabPin(e, anchor, dest)) return;\n anchor.setAttribute('href', `${raw}#${TAB_ACCOUNT_HINT_KEY}=${pin.didOc}`);\n } catch {\n // never break a navigation over decoration\n }\n };\n window.addEventListener('click', onActivate, true);\n window.addEventListener('auxclick', onActivate, true);\n return () => {\n window.removeEventListener('click', onActivate, true);\n window.removeEventListener('auxclick', onActivate, true);\n };\n}\n\n/**\n * On load, if the URL carries `#oc-as=<did>`, adopt that account for\n * THIS tab: mint a pin for it via the host's `/api/auth/tab` (which\n * re-validates the did is in this browser's roster) and stash it in\n * sessionStorage, then strip the fragment from the address bar. Must run\n * BEFORE the provider's first `/api/auth/me` fetch so that fetch carries\n * the right pin.\n *\n * Returns the adopted `did:oc` on success, else null. Best-effort and\n * fail-safe: a stale host (no targeted minting), a roster miss (403), or\n * a network error leaves the tab unpinned — exactly the legacy\n * cookie-following behavior. The fragment is always stripped so it never\n * lingers in history/bookmarks or re-fires on reload.\n */\nexport async function consumeTabAccountHint(authOrigin: string): Promise<string | null> {\n if (typeof window === 'undefined') return null;\n const frag = window.location.hash.replace(/^#/, '');\n const m = OC_AS_HINT_RE.exec(frag);\n if (!m) return null;\n const did = m[1]!;\n // Strip the fragment regardless of what happens next.\n try {\n const url = new URL(window.location.href);\n url.hash = '';\n window.history.replaceState(window.history.state, '', url.toString());\n } catch {\n // cosmetic only\n }\n // Already pinned to the hinted account (same-tab nav, or a same-origin\n // new tab that cloned sessionStorage) → nothing to mint.\n const existing = readTabSession();\n if (existing && existing.didOc === did) return did;\n try {\n const res = await fetch(`${authOrigin}/api/auth/tab`, {\n method: 'POST',\n credentials: 'include',\n headers: { Accept: 'application/json', 'Content-Type': 'application/json' },\n body: JSON.stringify({ did_oc: did }),\n });\n if (!res.ok) return null;\n const body = (await res.json()) as {\n ok?: boolean;\n token?: string;\n account?: { did_oc?: string };\n };\n // Only pin when the host minted for the hinted account — a stale\n // host that ignores the body mints for the cookie account, which\n // we must NOT adopt (it's the wrong-account bug we're fixing).\n if (body.ok && typeof body.token === 'string' && body.account?.did_oc === did) {\n writeTabSession({ token: body.token, didOc: did });\n return did;\n }\n } catch {\n // best-effort by construction\n }\n return null;\n}\n"],"mappings":";AAsBO,IAAM,qBAAqB;AAE3B,IAAM,0BAA0B;AAchC,IAAM,uBAAuB;AAQ7B,IAAM,iBAAiB;AAOvB,SAAS,iBAAsC;AAClD,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACA,UAAM,MAAM,OAAO,eAAe,QAAQ,uBAAuB;AACjE,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,OAAO,OAAO,UAAU,YAAY,OAAO,MAAM,WAAW,EAAG,QAAO;AAC1E,QAAI,OAAO,OAAO,UAAU,YAAY,OAAO,MAAM,WAAW,EAAG,QAAO;AAC1E,WAAO,EAAE,OAAO,OAAO,OAAO,OAAO,OAAO,MAAM;AAAA,EACtD,QAAQ;AAEJ,WAAO;AAAA,EACX;AACJ;AAEO,SAAS,gBAAgB,SAA6B;AACzD,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACA,WAAO,eAAe,QAAQ,yBAAyB,KAAK,UAAU,OAAO,CAAC;AAAA,EAClF,QAAQ;AAAA,EAER;AACJ;AAEO,SAAS,kBAAwB;AACpC,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACA,WAAO,eAAe,WAAW,uBAAuB;AAAA,EAC5D,QAAQ;AAAA,EAER;AACJ;AAGO,SAAS,mBAA2C;AACvD,QAAM,MAAM,eAAe;AAC3B,SAAO,MAAM,EAAE,CAAC,kBAAkB,GAAG,IAAI,MAAM,IAAI,CAAC;AACxD;AAOA,SAAS,cAAc,KAAa,YAA6B;AAC7D,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACA,UAAM,IAAI,IAAI,IAAI,KAAK,OAAO,SAAS,IAAI;AAC3C,WAAO,EAAE,WAAW,OAAO,SAAS,UAAU,EAAE,WAAW;AAAA,EAC/D,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAgBO,SAAS,2BAA2B,YAAgC;AACvE,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,WAAW,OAAO;AACxB,QAAM,UAA+B,CAAC,OAAO,SAAS;AAClD,QAAI;AACA,YAAM,MAAM,eAAe;AAC3B,UAAI,KAAK;AACL,cAAM,MACF,OAAO,UAAU,WACX,QACA,iBAAiB,MACf,MAAM,OACN,MAAM;AAClB,YAAI,cAAc,KAAK,UAAU,GAAG;AAChC,gBAAM,UAAU,IAAI;AAAA,YAChB,MAAM,YAAY,iBAAiB,UAAU,MAAM,UAAU;AAAA,UACjE;AACA,cAAI,CAAC,QAAQ,IAAI,eAAe,KAAK,CAAC,QAAQ,IAAI,kBAAkB,GAAG;AACnE,oBAAQ,IAAI,oBAAoB,IAAI,KAAK;AACzC,mBAAO,EAAE,GAAG,MAAM,QAAQ;AAAA,UAC9B;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ,QAAQ;AAAA,IAER;AACA,WAAO,SAAS,KAAK,QAAQ,OAA4B,IAAI;AAAA,EACjE;AACA,SAAO,QAAQ;AACf,SAAO,MAAM;AAET,QAAI,OAAO,UAAU,QAAS,QAAO,QAAQ;AAAA,EACjD;AACJ;AAOO,SAAS,wBAAiC;AAC7C,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI,CAAC,OAAO,SAAS,KAAK,SAAS,eAAe,MAAM,CAAC,CAAC,EAAG,QAAO;AACpE,kBAAgB;AAChB,MAAI;AACA,UAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,QAAI,OAAO;AACX,WAAO,QAAQ,aAAa,OAAO,QAAQ,OAAO,IAAI,IAAI,SAAS,CAAC;AAAA,EACxE,QAAQ;AAAA,EAGR;AACA,SAAO;AACX;AAcA,IAAM,gBAAgB;AAGtB,SAAS,WAAW,YAAmC;AACnD,MAAI;AACA,WAAO,IAAI,IAAI,UAAU,EAAE,SAAS,YAAY;AAAA,EACpD,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAQA,SAAS,YAAY,GAAQ,YAA6B;AACtD,MAAI,EAAE,aAAa,WAAW,EAAE,aAAa,SAAU,QAAO;AAC9D,MAAI,OAAO,WAAW,eAAe,EAAE,WAAW,OAAO,SAAS,OAAQ,QAAO;AACjF,QAAM,OAAO,WAAW,UAAU;AAClC,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,OAAO,EAAE,SAAS,YAAY;AACpC,SAAO,SAAS,QAAQ,KAAK,SAAS,IAAI,IAAI,EAAE;AACpD;AAGA,SAAS,mBAAmB,GAAe,QAA2B,MAAoB;AAEtF,MAAI,OAAO,WAAW,eAAe,KAAK,WAAW,OAAO,SAAS,OAAQ,QAAO;AAIpF,QAAM,UAAU,OAAO,aAAa,QAAQ,KAAK,IAAI,YAAY;AACjE,SACI,EAAE,WAAW;AAAA,EACb,EAAE,WACF,EAAE,WACF,EAAE,YACF,WAAW;AAEnB;AAsBO,SAAS,wBAAwB,YAAgC;AACpE,MAAI,OAAO,WAAW,eAAe,OAAO,aAAa,YAAa,QAAO,MAAM;AAAA,EAAC;AACpF,QAAM,aAAa,CAAC,MAAkB;AAClC,QAAI;AACA,YAAM,MAAM,eAAe;AAC3B,UAAI,CAAC,IAAK;AACV,YAAM,SAAS,EAAE;AACjB,UAAI,EAAE,kBAAkB,SAAU;AAClC,YAAM,SAAS,OAAO,QAAQ,SAAS;AACvC,UAAI,CAAC,UAAU,OAAO,aAAa,UAAU,EAAG;AAChD,YAAM,MAAM,OAAO,aAAa,MAAM;AACtC,UAAI,CAAC,OAAO,IAAI,SAAS,GAAG,oBAAoB,GAAG,EAAG;AACtD,UAAI;AACJ,UAAI;AACA,eAAO,IAAI,IAAI,OAAO,IAAI;AAAA,MAC9B,QAAQ;AACJ;AAAA,MACJ;AACA,UAAI,KAAK,KAAM;AACf,UAAI,CAAC,YAAY,MAAM,UAAU,EAAG;AACpC,UAAI,CAAC,mBAAmB,GAAG,QAAQ,IAAI,EAAG;AAC1C,aAAO,aAAa,QAAQ,GAAG,GAAG,IAAI,oBAAoB,IAAI,IAAI,KAAK,EAAE;AAAA,IAC7E,QAAQ;AAAA,IAER;AAAA,EACJ;AACA,SAAO,iBAAiB,SAAS,YAAY,IAAI;AACjD,SAAO,iBAAiB,YAAY,YAAY,IAAI;AACpD,SAAO,MAAM;AACT,WAAO,oBAAoB,SAAS,YAAY,IAAI;AACpD,WAAO,oBAAoB,YAAY,YAAY,IAAI;AAAA,EAC3D;AACJ;AAgBA,eAAsB,sBAAsB,YAA4C;AACpF,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAM,OAAO,OAAO,SAAS,KAAK,QAAQ,MAAM,EAAE;AAClD,QAAM,IAAI,cAAc,KAAK,IAAI;AACjC,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,MAAM,EAAE,CAAC;AAEf,MAAI;AACA,UAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,QAAI,OAAO;AACX,WAAO,QAAQ,aAAa,OAAO,QAAQ,OAAO,IAAI,IAAI,SAAS,CAAC;AAAA,EACxE,QAAQ;AAAA,EAER;AAGA,QAAM,WAAW,eAAe;AAChC,MAAI,YAAY,SAAS,UAAU,IAAK,QAAO;AAC/C,MAAI;AACA,UAAM,MAAM,MAAM,MAAM,GAAG,UAAU,iBAAiB;AAAA,MAClD,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,EAAE,QAAQ,oBAAoB,gBAAgB,mBAAmB;AAAA,MAC1E,MAAM,KAAK,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IACxC,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAQ,MAAM,IAAI,KAAK;AAQ7B,QAAI,KAAK,MAAM,OAAO,KAAK,UAAU,YAAY,KAAK,SAAS,WAAW,KAAK;AAC3E,sBAAgB,EAAE,OAAO,KAAK,OAAO,OAAO,IAAI,CAAC;AACjD,aAAO;AAAA,IACX;AAAA,EACJ,QAAQ;AAAA,EAER;AACA,SAAO;AACX;","names":[]}
@@ -0,0 +1,37 @@
1
+ import {
2
+ stripFragment
3
+ } from "./chunk-CYR7J4UY.mjs";
4
+
5
+ // src/types.ts
6
+ var DISPLAY_IDENTITY_KINDS = ["did", "btc", "email", "nostr"];
7
+ var DEFAULT_CONFIG = {
8
+ authOrigin: "https://ochk.io",
9
+ signInPath: "/signin",
10
+ mePath: "/api/auth/me",
11
+ logoutPath: "/api/auth/logout",
12
+ multiAccount: true
13
+ };
14
+ function resolveConfig(cfg) {
15
+ return { ...DEFAULT_CONFIG, ...cfg ?? {} };
16
+ }
17
+ function buildSignInUrl(cfg, returnTo) {
18
+ const base = `${cfg.authOrigin}${cfg.signInPath}`;
19
+ if (!returnTo) return base;
20
+ const u = new URL(base);
21
+ u.searchParams.set("return_to", stripFragment(returnTo));
22
+ return u.toString();
23
+ }
24
+ function buildAddAccountUrl(cfg, returnTo) {
25
+ const u = new URL(buildSignInUrl(cfg, returnTo));
26
+ u.searchParams.set("add", "1");
27
+ return u.toString();
28
+ }
29
+
30
+ export {
31
+ DISPLAY_IDENTITY_KINDS,
32
+ DEFAULT_CONFIG,
33
+ resolveConfig,
34
+ buildSignInUrl,
35
+ buildAddAccountUrl
36
+ };
37
+ //# sourceMappingURL=chunk-VRLFZAGK.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import { stripFragment } from './return-to';\n\n/**\n * The kinds of identity a user can surface as their account-badge\n * label. `did` is the canonical `did:oc` identifier — always available\n * and the ultimate fallback; the other three are linked identities a\n * user may or may not have.\n */\nexport const DISPLAY_IDENTITY_KINDS = ['did', 'btc', 'email', 'nostr'] as const;\nexport type DisplayIdentityKind = (typeof DISPLAY_IDENTITY_KINDS)[number];\n\n/**\n * The identity a user has chosen to show in their account badge — the\n * collapsed label every family site renders instead of the raw\n * `did:oc`. Carried as a JWT claim by the auth host, so the choice is\n * consistent across every `.ochk.io` subdomain with no round-trip.\n *\n * `kind` is which identity; `value` is the full, renderable value (the\n * `did:oc`, the Bitcoin address, the email, or the npub). Integrators\n * rendering their own chip read `useOcSession().account.displayIdentity`\n * — it is always populated (it defaults to the `did`).\n */\nexport interface DisplayIdentity {\n kind: DisplayIdentityKind;\n value: string;\n}\n\nexport interface OcAccount {\n accountId: string;\n /**\n * Opaque public-facing identifier · `did:oc:<32-hex>`. The sole\n * user identifier post auth-refactor. Stable across linking\n * events. Per AUTH-REFACTOR-PLAN.md §2.1.\n */\n didOc: string;\n /**\n * Primary linked Bitcoin address, plaintext, when the user has\n * one — null for email-OTP users until they link a btc address.\n * Surfaced inline by the auth host's /api/auth/me so consumer\n * dashboards can render the user's own footprint without a\n * second round-trip through /api/auth/identities.\n */\n primaryBtc?: string | null;\n /**\n * True when the user has a primary email linked. Plaintext is\n * fetchable on demand via /api/auth/identities; not surfaced\n * here.\n */\n hasEmail?: boolean;\n displayName?: string | null;\n nostrNpub?: string | null;\n /**\n * Slug of the federation this user is bound to (their \"home\"\n * federation). Multi-federation routing reads this; null/undefined\n * means \"not yet bound — fall back to the directory default at\n * /api/federations\". v1 has one live federation, so this is set on\n * first signin and rarely changes.\n */\n homeFederation?: string | null;\n /**\n * Where this user is on the custody-state graph:\n *\n * - 'fedimint_threshold' — federation custody (OC-introduced)\n * - 'fedimint_client' — federation custody (user-picked)\n * - 'bip322' — full self-custody\n *\n * Graduation is the product thesis. Treat undefined as\n * 'fedimint_threshold' for `did:email:` addresses and 'bip322' for\n * Bitcoin addresses (the default-by-construction mapping for\n * tokens minted before this field shipped).\n */\n signingMethod?: 'fedimint_threshold' | 'fedimint_client' | 'bip322' | null;\n /**\n * Best-effort owner-flag · true when the user's `did_oc` was on\n * the auth host's `OWNER_OC_ADDRESSES` env at the time the JWT\n * was minted. Surfaced so the family-switcher and other low-\n * stakes UX can render owner-only affordances (e.g. an\n * analytics.ochk.io entry visible only to owners).\n *\n * **NOT A SECURITY BOUNDARY.** Sensitive surfaces — including\n * analytics.ochk.io itself — re-check the live env against\n * `session.did_oc` server-side on every request. If an owner is\n * removed from the env, their JWT may keep `isOwner: true` for\n * up to the JWT lifetime (~30d) but every gated action they\n * attempt re-fails. The flag exists purely so we can show or\n * hide UX hints without an extra round-trip on every page load.\n *\n * Treat absence as `false`.\n */\n isOwner?: boolean;\n /**\n * The identity the user has chosen to show as their account-badge\n * label — `{ kind, value }`. **Always populated**: when the user\n * has never promoted an identity (and on sessions minted before\n * the feature shipped) this is `{ kind:'did', value:didOc }`.\n *\n * `<OcAccountMenu>` renders `value` (shortened) as the collapsed\n * badge label. Integrators rendering their own chip read this\n * directly; change it with `useOcSession().setDisplayIdentity()`.\n */\n displayIdentity: DisplayIdentity;\n}\n\nexport type OcSessionStatus = 'loading' | 'authenticated' | 'anonymous' | 'error';\n\n/**\n * Multi-account roster · summary of another account this browser has\n * signed into and can switch to without re-authenticating. Surfaced by\n * `<OcAccountMenu>` in the `§ accounts` section. Returned by the auth\n * host's `/api/auth/me` as `roster: [...]` (an empty array on single-\n * account or pre-multi-account sessions).\n *\n * Sessions for peer accounts stay alive in the auth host's DB for the\n * normal 30-day window; once expired, the peer falls out of the roster\n * and the user must re-authenticate to bring it back in.\n */\nexport interface OcAccountSummary {\n didOc: string;\n displayName: string | null;\n primaryBtc: string | null;\n displayIdentity: DisplayIdentity;\n /** ISO timestamp of the last verified request on this account's\n * session row, or null if never touched. */\n lastSeenAt: string | null;\n}\n\n/**\n * Scope hint for `signOut()` in a multi-account browser.\n *\n * - 'all' · sign out of every account in this browser's roster\n * (the default; matches the historical single-account\n * behavior of \"clear the cookie\").\n * - 'current' · sign out of just the active account; if the roster\n * has other accounts, the auth host hands you off to\n * the next-most-recent one without a re-auth.\n */\nexport type OcSignOutScope = 'all' | 'current';\n\nexport interface OcSessionState {\n status: OcSessionStatus;\n account: OcAccount | null;\n /**\n * Multi-account · other accounts the user has signed into in this\n * browser. `[]` for single-account sessions and for tokens minted\n * before multi-account shipped. The active account (`account`\n * above) is NOT included — these are the *switch targets*.\n */\n roster: OcAccountSummary[];\n /**\n * Per-tab pinning · `true` when this tab holds its own session\n * token (sessionStorage) and operates as `account` independently of\n * the shared `.ochk.io` cookie — i.e. switching accounts in another\n * tab will NOT change this tab's identity. `false` on hosts that\n * predate `/api/auth/tab`, in privacy modes without sessionStorage,\n * and while anonymous.\n */\n tabPinned: boolean;\n /** `null` while loading; an `Error` instance when `status === 'error'`. */\n error: Error | null;\n /** Re-fetch the session. Useful after sign-in/sign-out happens elsewhere. */\n refresh: () => Promise<void>;\n /**\n * Trigger a sign-out. By default signs out of EVERY account in the\n * roster (back-compat); pass `{ scope: 'current' }` to sign out of\n * just the active account and stay logged into the next peer.\n */\n signOut: (opts?: { scope?: OcSignOutScope }) => Promise<void>;\n /**\n * Multi-account · flip the active session to a different account\n * in the roster. Resolves once the new cookie has been set and\n * the session has been re-fetched. Throws if the target `did_oc`\n * isn't in the current roster (the user must add it via\n * {@link addAccount} first) or if the auth host is unreachable.\n */\n switchAccount: (didOc: string) => Promise<void>;\n /**\n * Multi-account · open the sign-in flow in \"add\" mode — the new\n * account joins the current browser's roster instead of replacing\n * it. Returns the URL to navigate to (so callers can mount it in\n * a popup or hard-navigate as they prefer); pass `returnTo` to\n * route back to a specific page after the add completes.\n */\n addAccountUrl: (returnTo?: string) => string;\n /**\n * Promote a linked identity to be the account-badge label across\n * every `.ochk.io` site. PATCHes the auth host, which re-mints the\n * session cookie with the new `display_identity` claim, then\n * `refresh()`es this session. Rejects if the chosen kind is not a\n * verified identity on the account (`btc` / `email` / `nostr` must\n * actually be linked; `did` is always valid).\n */\n setDisplayIdentity: (kind: DisplayIdentityKind) => Promise<void>;\n /** URL to navigate to for sign-in on the auth host. */\n signInUrl: string;\n}\n\nexport interface OcAuthConfig {\n /**\n * Origin of the auth host — the subdomain that runs the sign-in UI,\n * issues session cookies, and exposes `/api/auth/me` + `/api/auth/logout`.\n *\n * Defaults to `https://ochk.io`. Override in preview/dev.\n */\n authOrigin?: string;\n /**\n * Path on the auth host that accepts `?return_to=<url>` and drives the\n * sign-in flow. The page offers two paths in-place:\n *\n * - email + OTP (default — no wallet and no key of the user's own;\n * identity is `did:email:<sha256(email)>`)\n * - BIP-322 wallet sign (paste address → in-page wallet sign;\n * identity is the Bitcoin address itself)\n *\n * Defaults to `/signin`.\n */\n signInPath?: string;\n /**\n * Local path (same origin as the current app) that exposes the\n * crypto-verified session. If your app ships one at `/api/auth/me`,\n * leave as default. Returns 200 `{ account }` or 401.\n */\n mePath?: string;\n /**\n * Path on the auth host to hit to clear the session cookie.\n * Defaults to `/api/auth/logout`. Called with `credentials: 'include'`\n * so the `.ochk.io` cookie is sent along.\n */\n logoutPath?: string;\n /**\n * Multi-account and per-tab pinning against the auth host. Defaults to\n * `true`. A site that holds its own single session cookie (ochk.app)\n * sets `false`: no tab pins, no account hints or link stamping, and no\n * roster fetched from the host.\n */\n multiAccount?: boolean;\n}\n\nexport const DEFAULT_CONFIG: Required<OcAuthConfig> = {\n authOrigin: 'https://ochk.io',\n signInPath: '/signin',\n mePath: '/api/auth/me',\n logoutPath: '/api/auth/logout',\n multiAccount: true,\n};\n\nexport function resolveConfig(cfg: OcAuthConfig | undefined): Required<OcAuthConfig> {\n return { ...DEFAULT_CONFIG, ...(cfg ?? {}) };\n}\n\n/**\n * The sign-in URL on the auth host. `returnTo` goes without its fragment;\n * OcSessionProvider keeps the fragment when the link is followed.\n */\nexport function buildSignInUrl(cfg: Required<OcAuthConfig>, returnTo?: string): string {\n const base = `${cfg.authOrigin}${cfg.signInPath}`;\n if (!returnTo) return base;\n const u = new URL(base);\n u.searchParams.set('return_to', stripFragment(returnTo));\n return u.toString();\n}\n\n/**\n * Multi-account · build the URL for the \"add another account\" entry\n * point. Same as {@link buildSignInUrl} but appends `?add=1` so the\n * sign-in page knows to preserve the current roster (the new account\n * is appended; the previously-active account stays signed in too and\n * remains switch-target reachable).\n */\nexport function buildAddAccountUrl(cfg: Required<OcAuthConfig>, returnTo?: string): string {\n const u = new URL(buildSignInUrl(cfg, returnTo));\n u.searchParams.set('add', '1');\n return u.toString();\n}\n"],"mappings":";;;;;AAQO,IAAM,yBAAyB,CAAC,OAAO,OAAO,SAAS,OAAO;AAqO9D,IAAM,iBAAyC;AAAA,EAClD,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,cAAc;AAClB;AAEO,SAAS,cAAc,KAAuD;AACjF,SAAO,EAAE,GAAG,gBAAgB,GAAI,OAAO,CAAC,EAAG;AAC/C;AAMO,SAAS,eAAe,KAA6B,UAA2B;AACnF,QAAM,OAAO,GAAG,IAAI,UAAU,GAAG,IAAI,UAAU;AAC/C,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,IAAI,IAAI,IAAI,IAAI;AACtB,IAAE,aAAa,IAAI,aAAa,cAAc,QAAQ,CAAC;AACvD,SAAO,EAAE,SAAS;AACtB;AASO,SAAS,mBAAmB,KAA6B,UAA2B;AACvF,QAAM,IAAI,IAAI,IAAI,eAAe,KAAK,QAAQ,CAAC;AAC/C,IAAE,aAAa,IAAI,OAAO,GAAG;AAC7B,SAAO,EAAE,SAAS;AACtB;","names":[]}
@@ -0,0 +1,40 @@
1
+ import {
2
+ resolveConfig
3
+ } from "./chunk-VRLFZAGK.mjs";
4
+ import {
5
+ withoutFragment
6
+ } from "./chunk-CYR7J4UY.mjs";
7
+
8
+ // src/sudo.tsx
9
+ function redirectToSudo(args = {}) {
10
+ if (typeof window === "undefined") {
11
+ throw new Error(
12
+ "[@orangecheck/auth-client] redirectToSudo() requires a browser environment"
13
+ );
14
+ }
15
+ const cfg = resolveConfig(args.config);
16
+ const url = new URL("/sudo", cfg.authOrigin);
17
+ const returnTo = withoutFragment(args.returnTo ?? window.location.href);
18
+ if (returnTo) url.searchParams.set("return_to", returnTo);
19
+ if (args.purpose) url.searchParams.set("purpose", args.purpose);
20
+ window.location.assign(url.toString());
21
+ }
22
+ function handleSudoRequired(body, args = {}) {
23
+ if (body && body.reason === "sudo_required") {
24
+ redirectToSudo(args);
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+ var SUDO_ACCOUNT_MISMATCH_MESSAGE = "Switch this tab to that account first \u2014 the confirmation on file is for a different one.";
30
+ function isSudoAccountMismatch(body) {
31
+ return body?.reason === "sudo_account_mismatch";
32
+ }
33
+
34
+ export {
35
+ redirectToSudo,
36
+ handleSudoRequired,
37
+ SUDO_ACCOUNT_MISMATCH_MESSAGE,
38
+ isSudoAccountMismatch
39
+ };
40
+ //# sourceMappingURL=chunk-XCZVSE7M.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sudo.tsx"],"sourcesContent":["/**\n * Inline sudo-mode helpers · the consumer-side counterpart to the\n * ochk.io `/sudo` page.\n *\n * Pattern: when a consumer-side action hits a `401 sudo_required`\n * response from the auth host, call `redirectToSudo()` to bounce the\n * user to `https://ochk.io/sudo?return_to=<current>&purpose=<tag>`.\n * The user completes the email-OTP or BIP-322 re-authentication\n * inline on ochk.io; ochk.io re-issues the session JWT with a fresh\n * `sudo_at` claim; the browser is redirected back; the consumer's\n * original action succeeds on retry.\n *\n * No React hook needed for v1 · this is a one-shot navigation. A\n * future inline-modal version (no redirect) lives behind the same\n * function name so consumers don't have to change call-sites when we\n * upgrade.\n *\n * Server-side, gate sensitive endpoints with `verifySudoClaim()` from\n * @orangecheck/auth-core@^2.2.0.\n */\n\nimport { withoutFragment } from './return-to';\nimport { resolveConfig, type OcAuthConfig } from './types';\n\nexport interface RedirectToSudoArgs {\n /** Where to send the browser after the user finishes the sudo\n * ceremony. Defaults to this page at call-time. Its fragment is\n * never sent: it is kept in this tab and restored on return (see\n * ./return-to). Must be a family origin (`*.ochk.io`) or a relative\n * path; the /sudo page coerces unsafe values to /dashboard. */\n returnTo?: string;\n /** Free-form tag echoed in the /sudo page UI — e.g. `\"register a\n * hardware key\"`. The user sees \"To <purpose>, confirm with the\n * one-time code…\" so the prompt is framed by the action that\n * triggered it. Not security-relevant. */\n purpose?: string;\n /** Optional auth-client config override · same shape as\n * `<OcSessionProvider config={…} />`. Defaults to\n * `authOrigin: 'https://ochk.io'`. */\n config?: OcAuthConfig;\n}\n\n/**\n * Redirect the current browser to the auth host's `/sudo` page,\n * preserving `return_to` and `purpose` query parameters. The auth\n * host runs the ceremony and bounces back on success.\n *\n * Throws on SSR — call this only inside a click handler or effect\n * after a `'sudo_required'` response.\n */\nexport function redirectToSudo(args: RedirectToSudoArgs = {}): void {\n if (typeof window === 'undefined') {\n throw new Error(\n '[@orangecheck/auth-client] redirectToSudo() requires a browser environment'\n );\n }\n const cfg = resolveConfig(args.config);\n const url = new URL('/sudo', cfg.authOrigin);\n const returnTo = withoutFragment(args.returnTo ?? window.location.href);\n if (returnTo) url.searchParams.set('return_to', returnTo);\n if (args.purpose) url.searchParams.set('purpose', args.purpose);\n window.location.assign(url.toString());\n}\n\n/**\n * Convenience wrapper · the most common consumer pattern is \"if the\n * fetch response's body has `reason === 'sudo_required'`, redirect.\"\n * Use this to keep that idiom one line:\n *\n * const r = await fetch(...);\n * const j = await r.json();\n * if (handleSudoRequired(j, { purpose: 'register hardware key' })) return;\n *\n * Returns `true` if a redirect happened (and the caller should\n * short-circuit), `false` otherwise.\n */\nexport function handleSudoRequired(\n body: { reason?: string } | null | undefined,\n args: Omit<RedirectToSudoArgs, 'returnTo'> & { returnTo?: string } = {}\n): boolean {\n if (body && body.reason === 'sudo_required') {\n redirectToSudo(args);\n return true;\n }\n return false;\n}\n\n/**\n * The message for `sudo_account_mismatch` — a fresh sudo proof exists, but for\n * a different account than this tab is acting as.\n *\n * Deliberately NOT a redirect. The ceremony runs on ochk.io for the cookie's\n * default account and cannot reach this tab's pinned account, so bouncing there\n * re-proves the wrong one forever. Callers must show this and stop.\n */\nexport const SUDO_ACCOUNT_MISMATCH_MESSAGE =\n 'Switch this tab to that account first — the confirmation on file is for a different one.';\n\n/**\n * True when the response says a sudo ceremony cannot resolve this request.\n * Pairs with {@link handleSudoRequired}: try the redirect first, then this.\n */\nexport function isSudoAccountMismatch(body: { reason?: string } | null | undefined): boolean {\n return body?.reason === 'sudo_account_mismatch';\n}\n"],"mappings":";;;;;;;;AAkDO,SAAS,eAAe,OAA2B,CAAC,GAAS;AAChE,MAAI,OAAO,WAAW,aAAa;AAC/B,UAAM,IAAI;AAAA,MACN;AAAA,IACJ;AAAA,EACJ;AACA,QAAM,MAAM,cAAc,KAAK,MAAM;AACrC,QAAM,MAAM,IAAI,IAAI,SAAS,IAAI,UAAU;AAC3C,QAAM,WAAW,gBAAgB,KAAK,YAAY,OAAO,SAAS,IAAI;AACtE,MAAI,SAAU,KAAI,aAAa,IAAI,aAAa,QAAQ;AACxD,MAAI,KAAK,QAAS,KAAI,aAAa,IAAI,WAAW,KAAK,OAAO;AAC9D,SAAO,SAAS,OAAO,IAAI,SAAS,CAAC;AACzC;AAcO,SAAS,mBACZ,MACA,OAAqE,CAAC,GAC/D;AACP,MAAI,QAAQ,KAAK,WAAW,iBAAiB;AACzC,mBAAe,IAAI;AACnB,WAAO;AAAA,EACX;AACA,SAAO;AACX;AAUO,IAAM,gCACT;AAMG,SAAS,sBAAsB,MAAuD;AACzF,SAAO,MAAM,WAAW;AAC5B;","names":[]}
@@ -0,0 +1,19 @@
1
+ import {
2
+ OcAccountChip,
3
+ OcAccountPill,
4
+ OcAddressInput,
5
+ OcSignInButton,
6
+ useOcAddressSuggestion
7
+ } from "./chunk-7UYPMTAV.mjs";
8
+ import "./chunk-7ST4Q5KR.mjs";
9
+ import "./chunk-VRLFZAGK.mjs";
10
+ import "./chunk-CYR7J4UY.mjs";
11
+ import "./chunk-TJJNTITX.mjs";
12
+ export {
13
+ OcAccountChip,
14
+ OcAccountPill,
15
+ OcAddressInput,
16
+ OcSignInButton,
17
+ useOcAddressSuggestion
18
+ };
19
+ //# sourceMappingURL=components.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}