@orangecheck/auth-client 2.25.3 → 2.26.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.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import * as React3 from 'react';
1
+ import * as React4 from 'react';
2
2
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
3
  import { startRegistration, startAuthentication } from '@simplewebauthn/browser';
4
4
 
@@ -199,7 +199,7 @@ async function consumeTabAccountHint(authOrigin) {
199
199
  }
200
200
  return null;
201
201
  }
202
- var SessionContext = React3.createContext(null);
202
+ var SessionContext = React4.createContext(null);
203
203
  function normalizeDisplayIdentity(raw, didOc) {
204
204
  const di = raw.display_identity ?? raw.displayIdentity;
205
205
  if (di && typeof di === "object" && typeof di.value === "string" && di.value.length > 0 && typeof di.kind === "string" && DISPLAY_IDENTITY_KINDS.includes(di.kind)) {
@@ -257,14 +257,14 @@ function OcSessionProvider({
257
257
  config,
258
258
  defaultReturnTo
259
259
  }) {
260
- const cfg = React3.useMemo(() => resolveConfig(config), [config]);
261
- const [account, setAccount] = React3.useState(null);
262
- const [roster, setRoster] = React3.useState([]);
263
- const [status, setStatus] = React3.useState("loading");
264
- const [error, setError] = React3.useState(null);
265
- const [tabPinned, setTabPinned] = React3.useState(false);
266
- const pinInFlightRef = React3.useRef(false);
267
- const pinThisTab = React3.useCallback(
260
+ const cfg = React4.useMemo(() => resolveConfig(config), [config]);
261
+ const [account, setAccount] = React4.useState(null);
262
+ const [roster, setRoster] = React4.useState([]);
263
+ const [status, setStatus] = React4.useState("loading");
264
+ const [error, setError] = React4.useState(null);
265
+ const [tabPinned, setTabPinned] = React4.useState(false);
266
+ const pinInFlightRef = React4.useRef(false);
267
+ const pinThisTab = React4.useCallback(
268
268
  async (didOc) => {
269
269
  if (pinInFlightRef.current || readTabSession()) return;
270
270
  pinInFlightRef.current = true;
@@ -287,7 +287,7 @@ function OcSessionProvider({
287
287
  },
288
288
  [cfg.authOrigin]
289
289
  );
290
- const refresh = React3.useCallback(async () => {
290
+ const refresh = React4.useCallback(async () => {
291
291
  if (typeof window === "undefined") return;
292
292
  try {
293
293
  let pin = readTabSession();
@@ -341,7 +341,7 @@ function OcSessionProvider({
341
341
  setError(err instanceof Error ? err : new Error(String(err)));
342
342
  }
343
343
  }, [cfg, pinThisTab]);
344
- React3.useEffect(() => {
344
+ React4.useEffect(() => {
345
345
  let cancelled = false;
346
346
  consumeTabAdoptMarker();
347
347
  void (async () => {
@@ -352,9 +352,9 @@ function OcSessionProvider({
352
352
  cancelled = true;
353
353
  };
354
354
  }, [refresh, cfg.authOrigin]);
355
- React3.useEffect(() => installTabFetchInterceptor(cfg.authOrigin), [cfg.authOrigin]);
356
- React3.useEffect(() => installTabLinkDecorator(cfg.authOrigin), [cfg.authOrigin]);
357
- const signOut = React3.useCallback(
355
+ React4.useEffect(() => installTabFetchInterceptor(cfg.authOrigin), [cfg.authOrigin]);
356
+ React4.useEffect(() => installTabLinkDecorator(cfg.authOrigin), [cfg.authOrigin]);
357
+ const signOut = React4.useCallback(
358
358
  async (opts) => {
359
359
  const scope = opts?.scope ?? "all";
360
360
  try {
@@ -390,7 +390,7 @@ function OcSessionProvider({
390
390
  },
391
391
  [cfg.authOrigin, cfg.logoutPath, refresh]
392
392
  );
393
- const switchAccount = React3.useCallback(
393
+ const switchAccount = React4.useCallback(
394
394
  async (didOc) => {
395
395
  if (typeof window === "undefined") return;
396
396
  const res = await fetch(`${cfg.authOrigin}/api/auth/switch`, {
@@ -420,14 +420,14 @@ function OcSessionProvider({
420
420
  },
421
421
  [cfg.authOrigin, refresh]
422
422
  );
423
- const addAccountUrl = React3.useCallback(
423
+ const addAccountUrl = React4.useCallback(
424
424
  (returnTo) => {
425
425
  const rt = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
426
426
  return buildAddAccountUrl(cfg, rt);
427
427
  },
428
428
  [cfg]
429
429
  );
430
- const setDisplayIdentity = React3.useCallback(
430
+ const setDisplayIdentity = React4.useCallback(
431
431
  async (kind) => {
432
432
  if (typeof window === "undefined") return;
433
433
  const res = await fetch(`${cfg.authOrigin}/api/auth/account`, {
@@ -457,7 +457,7 @@ function OcSessionProvider({
457
457
  },
458
458
  [cfg.authOrigin, refresh]
459
459
  );
460
- const value = React3.useMemo(() => {
460
+ const value = React4.useMemo(() => {
461
461
  const returnTo = defaultReturnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
462
462
  return {
463
463
  status,
@@ -489,7 +489,7 @@ function OcSessionProvider({
489
489
  return /* @__PURE__ */ jsx(SessionContext.Provider, { value, children });
490
490
  }
491
491
  function useOcSession() {
492
- const ctx = React3.useContext(SessionContext);
492
+ const ctx = React4.useContext(SessionContext);
493
493
  if (!ctx) {
494
494
  throw new Error(
495
495
  "[@orangecheck/auth-client] useOcSession() must be called inside <OcSessionProvider>"
@@ -498,7 +498,7 @@ function useOcSession() {
498
498
  return ctx;
499
499
  }
500
500
  function useOptionalOcSession() {
501
- return React3.useContext(SessionContext);
501
+ return React4.useContext(SessionContext);
502
502
  }
503
503
  function shortenAddress(addr) {
504
504
  if (addr.length <= 12) return addr;
@@ -513,7 +513,7 @@ function isPrefixOf(value, target) {
513
513
  }
514
514
  var listboxIdCounter = 0;
515
515
  function useUniqueId(prefix) {
516
- const [id] = React3.useState(() => `${prefix}-${++listboxIdCounter}`);
516
+ const [id] = React4.useState(() => `${prefix}-${++listboxIdCounter}`);
517
517
  return id;
518
518
  }
519
519
  function OcAccountChip({
@@ -527,9 +527,9 @@ function OcAccountChip({
527
527
  }) {
528
528
  const { status, account, signInUrl: sessionSignInUrl, signOut } = useOcSession();
529
529
  const signInUrl = signInUrlOverride ?? sessionSignInUrl;
530
- const [open, setOpen] = React3.useState(false);
531
- const wrapRef = React3.useRef(null);
532
- React3.useEffect(() => {
530
+ const [open, setOpen] = React4.useState(false);
531
+ const wrapRef = React4.useRef(null);
532
+ React4.useEffect(() => {
533
533
  if (!open) return;
534
534
  function handleClickOutside(e) {
535
535
  if (wrapRef.current && !wrapRef.current.contains(e.target)) {
@@ -852,9 +852,9 @@ function useOcAddressSuggestion(options) {
852
852
  } = options;
853
853
  const { status, account } = useOcSession();
854
854
  const sessionAddress = status === "authenticated" ? account?.primaryBtc ?? null : null;
855
- const [open, setOpen] = React3.useState(false);
856
- const [highlighted, setHighlighted] = React3.useState(false);
857
- const blurTimer = React3.useRef(null);
855
+ const [open, setOpen] = React4.useState(false);
856
+ const [highlighted, setHighlighted] = React4.useState(false);
857
+ const blurTimer = React4.useRef(null);
858
858
  const listboxId = useUniqueId("oc-addr-listbox");
859
859
  const optionId = `${listboxId}-opt`;
860
860
  const valueMatchesSession = sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();
@@ -866,7 +866,7 @@ function useOcAddressSuggestion(options) {
866
866
  setOpen(false);
867
867
  setHighlighted(false);
868
868
  }
869
- React3.useEffect(() => {
869
+ React4.useEffect(() => {
870
870
  return () => {
871
871
  if (blurTimer.current != null) window.clearTimeout(blurTimer.current);
872
872
  };
@@ -969,7 +969,7 @@ function useOcAddressSuggestion(options) {
969
969
  ) : null;
970
970
  return { inputProps, popover };
971
971
  }
972
- var OcAddressInput = React3.forwardRef(
972
+ var OcAddressInput = React4.forwardRef(
973
973
  function OcAddressInput2({
974
974
  value,
975
975
  onValueChange,
@@ -984,15 +984,15 @@ var OcAddressInput = React3.forwardRef(
984
984
  }, forwardedRef) {
985
985
  const { status, account } = useOcSession();
986
986
  const sessionAddress = status === "authenticated" ? account?.primaryBtc ?? null : null;
987
- const [open, setOpen] = React3.useState(false);
988
- const [highlighted, setHighlighted] = React3.useState(false);
989
- const innerRef = React3.useRef(null);
987
+ const [open, setOpen] = React4.useState(false);
988
+ const [highlighted, setHighlighted] = React4.useState(false);
989
+ const innerRef = React4.useRef(null);
990
990
  const setRef = (node) => {
991
991
  innerRef.current = node;
992
992
  if (typeof forwardedRef === "function") forwardedRef(node);
993
993
  else if (forwardedRef) forwardedRef.current = node;
994
994
  };
995
- const blurTimer = React3.useRef(null);
995
+ const blurTimer = React4.useRef(null);
996
996
  const listboxId = useUniqueId("oc-addr-listbox");
997
997
  const optionId = `${listboxId}-opt`;
998
998
  const valueMatchesSession = sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();
@@ -1039,7 +1039,7 @@ var OcAddressInput = React3.forwardRef(
1039
1039
  }
1040
1040
  onKeyDown?.(e);
1041
1041
  }
1042
- React3.useEffect(() => {
1042
+ React4.useEffect(() => {
1043
1043
  return () => {
1044
1044
  if (blurTimer.current != null) window.clearTimeout(blurTimer.current);
1045
1045
  };
@@ -1173,13 +1173,14 @@ async function fetchOcLinkedIdentities(opts) {
1173
1173
  function OcLinkedIdentities({
1174
1174
  authOrigin = DEFAULT_AUTH_ORIGIN,
1175
1175
  className,
1176
- onChange
1176
+ onChange,
1177
+ attestationLink = true
1177
1178
  }) {
1178
- const [resp, setResp] = React3.useState(null);
1179
- const [error, setError] = React3.useState(null);
1180
- const [showEmailLink, setShowEmailLink] = React3.useState(false);
1181
- const [showBtcLink, setShowBtcLink] = React3.useState(false);
1182
- const refresh = React3.useCallback(async () => {
1179
+ const [resp, setResp] = React4.useState(null);
1180
+ const [error, setError] = React4.useState(null);
1181
+ const [showEmailLink, setShowEmailLink] = React4.useState(false);
1182
+ const [showBtcLink, setShowBtcLink] = React4.useState(false);
1183
+ const refresh = React4.useCallback(async () => {
1183
1184
  setError(null);
1184
1185
  try {
1185
1186
  const r = await fetch(`${authOrigin}/api/auth/identities`, {
@@ -1198,10 +1199,10 @@ function OcLinkedIdentities({
1198
1199
  setError(e instanceof Error ? e.message : "load failed");
1199
1200
  }
1200
1201
  }, [authOrigin]);
1201
- React3.useEffect(() => {
1202
+ React4.useEffect(() => {
1202
1203
  void refresh();
1203
1204
  }, [refresh]);
1204
- const afterChange = React3.useCallback(() => {
1205
+ const afterChange = React4.useCallback(() => {
1205
1206
  setShowEmailLink(false);
1206
1207
  setShowBtcLink(false);
1207
1208
  void refresh();
@@ -1233,11 +1234,7 @@ function OcLinkedIdentities({
1233
1234
  return /* @__PURE__ */ jsxs("div", { className, "data-oc-li": "", style: { display: "grid", gap: 16 }, children: [
1234
1235
  /* @__PURE__ */ jsxs("div", { style: panelStyle(), children: [
1235
1236
  /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 what this is" }),
1236
- /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1237
- "Your public identity is the opaque ",
1238
- /* @__PURE__ */ jsx("code", { style: codeStyle, children: "did:oc:" }),
1239
- " DID. Below it sit your private linked identities \u2014 your email and your Bitcoin address. They never appear in dashboard chrome and never reach an integrator unless you grant the matching scope. Linking a second identity is also your account-recovery path: if you lose one, the other still signs you in."
1240
- ] })
1237
+ /* @__PURE__ */ jsx("p", { style: bodyStyle, children: "These are the ways you can sign in to this account. An app you sign in to sees one only if you grant it. Linking a second one is also your way back in: lose one, and the other still signs you in." })
1241
1238
  ] }),
1242
1239
  mergedFrom.length > 0 && /* @__PURE__ */ jsx(ConsolidationPanel, { entries: mergedFrom }),
1243
1240
  /* @__PURE__ */ jsxs("div", { style: panelStyle("plain"), "data-oc-li-rows": "", children: [
@@ -1290,7 +1287,7 @@ function OcLinkedIdentities({
1290
1287
  onCancel: () => setShowBtcLink(false)
1291
1288
  }
1292
1289
  ),
1293
- hasBtc && /* @__PURE__ */ jsx(AttestationLink, {})
1290
+ hasBtc && attestationLink && /* @__PURE__ */ jsx(AttestationLink, {})
1294
1291
  ] });
1295
1292
  }
1296
1293
  function IdentityGroup({
@@ -1334,10 +1331,10 @@ function IdentityRow({
1334
1331
  authOrigin,
1335
1332
  onChange
1336
1333
  }) {
1337
- const [shown, setShown] = React3.useState(false);
1338
- const [confirming, setConfirming] = React3.useState(false);
1339
- const [busy, setBusy] = React3.useState(false);
1340
- const [err, setErr] = React3.useState(null);
1334
+ const [shown, setShown] = React4.useState(false);
1335
+ const [confirming, setConfirming] = React4.useState(false);
1336
+ const [busy, setBusy] = React4.useState(false);
1337
+ const [err, setErr] = React4.useState(null);
1341
1338
  const masked = row.value ? mask(row.value, kind) : "\u2014 pending recovery \u2014";
1342
1339
  const display = shown && row.value ? row.value : masked;
1343
1340
  async function unlink() {
@@ -1536,13 +1533,13 @@ function EmailLinkForm({
1536
1533
  onDone,
1537
1534
  onCancel
1538
1535
  }) {
1539
- const [step, setStep] = React3.useState("email");
1540
- const [email, setEmail] = React3.useState("");
1541
- const [code, setCode] = React3.useState("");
1542
- const [token, setToken] = React3.useState("");
1543
- const [busy, setBusy] = React3.useState(false);
1544
- const [err, setErr] = React3.useState(null);
1545
- const [transfer, setTransfer] = React3.useState(false);
1536
+ const [step, setStep] = React4.useState("email");
1537
+ const [email, setEmail] = React4.useState("");
1538
+ const [code, setCode] = React4.useState("");
1539
+ const [token, setToken] = React4.useState("");
1540
+ const [busy, setBusy] = React4.useState(false);
1541
+ const [err, setErr] = React4.useState(null);
1542
+ const [transfer, setTransfer] = React4.useState(false);
1546
1543
  async function start() {
1547
1544
  setBusy(true);
1548
1545
  setErr(null);
@@ -1685,12 +1682,12 @@ function BtcLinkForm({
1685
1682
  onDone,
1686
1683
  onCancel
1687
1684
  }) {
1688
- const [step, setStep] = React3.useState("request");
1689
- const [addr, setAddr] = React3.useState("");
1690
- const [challenge, setChallenge] = React3.useState("");
1691
- const [busy, setBusy] = React3.useState(false);
1692
- const [err, setErr] = React3.useState(null);
1693
- const [pendingSig, setPendingSig] = React3.useState(null);
1685
+ const [step, setStep] = React4.useState("request");
1686
+ const [addr, setAddr] = React4.useState("");
1687
+ const [challenge, setChallenge] = React4.useState("");
1688
+ const [busy, setBusy] = React4.useState(false);
1689
+ const [err, setErr] = React4.useState(null);
1690
+ const [pendingSig, setPendingSig] = React4.useState(null);
1694
1691
  async function getChallenge() {
1695
1692
  if (!didOc) {
1696
1693
  setErr("session not loaded yet \xB7 refresh and retry");
@@ -1850,9 +1847,9 @@ function TransferConfirm({
1850
1847
  ] });
1851
1848
  }
1852
1849
  function LazyWalletButton(props) {
1853
- const [Comp, setComp] = React3.useState(null);
1854
- const [failed, setFailed] = React3.useState(false);
1855
- React3.useEffect(() => {
1850
+ const [Comp, setComp] = React4.useState(null);
1851
+ const [failed, setFailed] = React4.useState(false);
1852
+ React4.useEffect(() => {
1856
1853
  let alive = true;
1857
1854
  import('@orangecheck/wallet-adapter/react').then((m) => {
1858
1855
  if (alive) {
@@ -1960,10 +1957,6 @@ var hintStyle = {
1960
1957
  fontSize: 10.5,
1961
1958
  opacity: 0.8
1962
1959
  };
1963
- var codeStyle = {
1964
- fontFamily: MONO,
1965
- color: V.foreground
1966
- };
1967
1960
  var rowStyle = {
1968
1961
  display: "flex",
1969
1962
  flexWrap: "wrap",
@@ -2070,6 +2063,50 @@ function hardNavigate(target) {
2070
2063
  if (typeof window === "undefined") return;
2071
2064
  window.location.assign(target);
2072
2065
  }
2066
+ var SIGNIN_ERRORS = {
2067
+ bad_request: "That doesn't look like a Bitcoin address.",
2068
+ invalid_challenge: "That signature doesn't match this address. Sign with the wallet that holds it.",
2069
+ sig_invalid: "That signature doesn't match this address. Sign with the wallet that holds it.",
2070
+ sig_unsupported_scheme: "We can't check this wallet's signature format yet. Try another wallet, or use email.",
2071
+ expired: "That request timed out. Try again for a fresh one.",
2072
+ not_yet_valid: "That request is not valid yet. Check your device clock, then try again.",
2073
+ nonce_mismatch: "That request didn't match. Try again for a fresh one.",
2074
+ audience_mismatch: "That request didn't match this site. Reload the page and try again.",
2075
+ purpose_mismatch: "That request didn't match. Try again for a fresh one.",
2076
+ malformed: "That signature couldn't be read. Copy it again, whole.",
2077
+ cross_site_blocked: "Your browser blocked this request. Reload the page and try again.",
2078
+ rate_limited: "Too many tries. Wait a minute, then try again.",
2079
+ invalid_email: "That email address doesn't look right.",
2080
+ mailer_unavailable: "We couldn't send the email right now. Try again in a minute.",
2081
+ mailer_error: "We couldn't send the email right now. Try again in a minute.",
2082
+ wrong_code: "That code isn't right. Check the newest email from us.",
2083
+ replay: "That code was already used. Send a new one.",
2084
+ invalid_token: "That code is no longer valid. Send a new one.",
2085
+ email_mismatch: "That code was sent to a different address. Send a new one."
2086
+ };
2087
+ var EMAIL_ERRORS = {
2088
+ expired: "That code expired. Send a new one."
2089
+ };
2090
+ var SUBMIT_ERRORS = {
2091
+ bad_request: "That signature couldn't be read. Copy it again, whole."
2092
+ };
2093
+ var NETWORK_ERROR = "We couldn't reach the server. Check your connection and try again.";
2094
+ function humanSigninError(reason, path, step = "challenge") {
2095
+ if (!/^[a-z0-9_]+$/.test(reason)) return reason;
2096
+ if (path === "wallet" && step === "submit" && SUBMIT_ERRORS[reason]) return SUBMIT_ERRORS[reason];
2097
+ if (path === "email" && EMAIL_ERRORS[reason]) return EMAIL_ERRORS[reason];
2098
+ return SIGNIN_ERRORS[reason] ?? (path === "email" ? "That didn't work. Try again, or send a new code." : "That didn't work. Try again.");
2099
+ }
2100
+ function failureMessage(err, path, step) {
2101
+ if (err instanceof TypeError || err instanceof SyntaxError) return NETWORK_ERROR;
2102
+ return humanSigninError(err instanceof Error ? err.message : "failed", path, step);
2103
+ }
2104
+ var BECH32_ADDRESS = /^(bc1|tb1|bcrt1)[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{8,87}$/i;
2105
+ var BASE58_ADDRESS = /^[123mn][1-9A-HJ-NP-Za-km-z]{25,34}$/;
2106
+ function looksLikeBitcoinAddress(input) {
2107
+ const s = input.trim();
2108
+ return BECH32_ADDRESS.test(s) || BASE58_ADDRESS.test(s);
2109
+ }
2073
2110
  function OcSignIn({
2074
2111
  audience,
2075
2112
  returnTo,
@@ -2085,10 +2122,10 @@ function OcSignIn({
2085
2122
  }) {
2086
2123
  const walletEnabled = paths?.wallet ?? true;
2087
2124
  const emailEnabled = paths?.email ?? true;
2088
- const [resolvedReturn, setResolvedReturn] = React3.useState(
2125
+ const [resolvedReturn, setResolvedReturn] = React4.useState(
2089
2126
  () => familyReturnTarget(returnTo) ?? safeReturnTo(returnTo)
2090
2127
  );
2091
- React3.useEffect(() => {
2128
+ React4.useEffect(() => {
2092
2129
  const fromProp = familyReturnTarget(returnTo);
2093
2130
  if (fromProp) {
2094
2131
  setResolvedReturn(fromProp);
@@ -2099,8 +2136,8 @@ function OcSignIn({
2099
2136
  const fromQuery = familyReturnTarget(q.get("return_to")) ?? familyReturnTarget(q.get("next"));
2100
2137
  setResolvedReturn(fromQuery ?? "/");
2101
2138
  }, [returnTo]);
2102
- const [addMode, setAddMode] = React3.useState(Boolean(addProp));
2103
- React3.useEffect(() => {
2139
+ const [addMode, setAddMode] = React4.useState(Boolean(addProp));
2140
+ React4.useEffect(() => {
2104
2141
  if (typeof window === "undefined") return;
2105
2142
  if (addProp !== void 0) {
2106
2143
  setAddMode(Boolean(addProp));
@@ -2108,19 +2145,19 @@ function OcSignIn({
2108
2145
  }
2109
2146
  setAddMode(new URLSearchParams(window.location.search).get("add") === "1");
2110
2147
  }, [addProp]);
2111
- const [path, setPath] = React3.useState(
2148
+ const [path, setPath] = React4.useState(
2112
2149
  initialPath ?? (providersFirst ? "email" : "wallet")
2113
2150
  );
2114
- const [signedIn, setSignedIn] = React3.useState(null);
2115
- const [linkAlso, setLinkAlso] = React3.useState(false);
2116
- const [oauthError, setOauthError] = React3.useState(false);
2117
- React3.useEffect(() => {
2151
+ const [signedIn, setSignedIn] = React4.useState(null);
2152
+ const [linkAlso, setLinkAlso] = React4.useState(false);
2153
+ const [oauthError, setOauthError] = React4.useState(false);
2154
+ React4.useEffect(() => {
2118
2155
  if (typeof window === "undefined") return;
2119
2156
  if (new URLSearchParams(window.location.search).has("oauth_error")) {
2120
2157
  setOauthError(true);
2121
2158
  }
2122
2159
  }, []);
2123
- const navigate = React3.useCallback(
2160
+ const navigate = React4.useCallback(
2124
2161
  async (account) => {
2125
2162
  if (resolveReturnTo) {
2126
2163
  try {
@@ -2134,7 +2171,7 @@ function OcSignIn({
2134
2171
  },
2135
2172
  [resolveReturnTo, resolvedReturn]
2136
2173
  );
2137
- const handleSuccess = React3.useCallback(
2174
+ const handleSuccess = React4.useCallback(
2138
2175
  async (account, token, via) => {
2139
2176
  const proceed = () => {
2140
2177
  clearTabSession();
@@ -2220,7 +2257,7 @@ function OcSignIn({
2220
2257
  fontFamily: "ui-monospace, SFMono-Regular, monospace",
2221
2258
  fontSize: 11
2222
2259
  },
2223
- children: "That sign-in didn't complete. Please try again."
2260
+ children: "That sign-in didn't finish. Try again, or use another way below."
2224
2261
  }
2225
2262
  ),
2226
2263
  addMode && /* @__PURE__ */ jsxs(
@@ -2285,7 +2322,7 @@ function OcSignIn({
2285
2322
  {
2286
2323
  active: path === "email",
2287
2324
  onClick: () => setPath("email"),
2288
- children: "email + otp"
2325
+ children: "email"
2289
2326
  }
2290
2327
  ),
2291
2328
  /* @__PURE__ */ jsx(
@@ -2293,7 +2330,7 @@ function OcSignIn({
2293
2330
  {
2294
2331
  active: path === "wallet",
2295
2332
  onClick: () => setPath("wallet"),
2296
- children: "bitcoin \xB7 self-custody"
2333
+ children: "bitcoin wallet"
2297
2334
  }
2298
2335
  )
2299
2336
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -2310,7 +2347,7 @@ function OcSignIn({
2310
2347
  {
2311
2348
  active: path === "email",
2312
2349
  onClick: () => setPath("email"),
2313
- children: "email + otp"
2350
+ children: "email"
2314
2351
  }
2315
2352
  )
2316
2353
  ] })
@@ -2343,7 +2380,7 @@ function OcSignIn({
2343
2380
  type: "checkbox",
2344
2381
  checked: linkAlso,
2345
2382
  onChange: (e) => setLinkAlso(e.target.checked),
2346
- style: { marginTop: 2, accentColor: "var(--primary, #f97316)" }
2383
+ style: { margin: 0, accentColor: "var(--primary, #f97316)" }
2347
2384
  }
2348
2385
  ),
2349
2386
  /* @__PURE__ */ jsxs("span", { children: [
@@ -2351,7 +2388,7 @@ function OcSignIn({
2351
2388
  " ",
2352
2389
  activeMethod === "email" ? "a Bitcoin wallet" : "an email",
2353
2390
  " ",
2354
- /* @__PURE__ */ jsx("span", { style: { opacity: 0.65 }, children: "\u2014 optional, one more signature." })
2391
+ /* @__PURE__ */ jsx("span", { children: "\u2014 optional, one more signature." })
2355
2392
  ] })
2356
2393
  ] })
2357
2394
  ] });
@@ -2370,6 +2407,7 @@ function SigninTab({
2370
2407
  onClick,
2371
2408
  "data-oc-signin-tab": active ? "active" : "inactive",
2372
2409
  style: {
2410
+ minHeight: 44,
2373
2411
  padding: "0.6rem 0.875rem",
2374
2412
  background: "transparent",
2375
2413
  border: "none",
@@ -2409,20 +2447,21 @@ function ProviderSignIn({
2409
2447
  add,
2410
2448
  first = false
2411
2449
  }) {
2412
- const [providers, setProviders] = React3.useState([]);
2413
- const [origin, setOrigin] = React3.useState("");
2414
- React3.useEffect(() => {
2450
+ const [providers, setProviders] = React4.useState(null);
2451
+ const [origin, setOrigin] = React4.useState("");
2452
+ React4.useEffect(() => {
2415
2453
  setOrigin(window.location.origin);
2416
2454
  let cancelled = false;
2417
2455
  fetch(`${authOrigin}/api/auth/providers`, { credentials: "include" }).then((r) => r.ok ? r.json() : null).then((body) => {
2418
- if (!cancelled && body?.providers) setProviders(body.providers);
2456
+ if (!cancelled) setProviders(body?.providers ?? []);
2419
2457
  }).catch(() => {
2458
+ if (!cancelled) setProviders([]);
2420
2459
  });
2421
2460
  return () => {
2422
2461
  cancelled = true;
2423
2462
  };
2424
2463
  }, [authOrigin]);
2425
- if (providers.length === 0) return null;
2464
+ if (providers !== null && providers.length === 0) return null;
2426
2465
  const providerReturnTo = returnTo.startsWith("/") ? origin ? `${origin}${returnTo}` : returnTo : returnTo;
2427
2466
  const line = { flex: 1, height: 1, background: "var(--border, #27272a)" };
2428
2467
  const BLOCK_GAP = 16;
@@ -2451,6 +2490,31 @@ function ProviderSignIn({
2451
2490
  ]
2452
2491
  }
2453
2492
  );
2493
+ const buttonStyle = (i) => ({
2494
+ display: "flex",
2495
+ alignItems: "center",
2496
+ justifyContent: "center",
2497
+ gap: 10,
2498
+ boxSizing: "border-box",
2499
+ width: "100%",
2500
+ minHeight: 44,
2501
+ marginTop: i === 0 ? 0 : BUTTON_GAP,
2502
+ padding: "0.6rem 0.875rem",
2503
+ border: "1px solid var(--border, #27272a)",
2504
+ borderRadius: 6,
2505
+ background: "transparent",
2506
+ color: "var(--muted-foreground, #a1a1aa)",
2507
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
2508
+ fontSize: 12,
2509
+ textDecoration: "none"
2510
+ });
2511
+ if (providers === null) {
2512
+ if (!first) return null;
2513
+ return /* @__PURE__ */ jsxs("div", { "data-oc-signin-providers": "", "aria-hidden": "true", children: [
2514
+ [0, 1].map((i) => /* @__PURE__ */ jsx("div", { style: { ...buttonStyle(i), visibility: "hidden" }, children: "\xA0" }, i)),
2515
+ divider
2516
+ ] });
2517
+ }
2454
2518
  const buttons = providers.map((p, i) => /* @__PURE__ */ jsxs(
2455
2519
  "a",
2456
2520
  {
@@ -2458,23 +2522,7 @@ function ProviderSignIn({
2458
2522
  providerReturnTo
2459
2523
  )}${add ? "&add=1" : ""}`,
2460
2524
  "data-oc-signin-provider": p.id,
2461
- style: {
2462
- display: "flex",
2463
- alignItems: "center",
2464
- justifyContent: "center",
2465
- gap: 10,
2466
- boxSizing: "border-box",
2467
- width: "100%",
2468
- marginTop: i === 0 ? 0 : BUTTON_GAP,
2469
- padding: "0.6rem 0.875rem",
2470
- border: "1px solid var(--border, #27272a)",
2471
- borderRadius: 6,
2472
- background: "transparent",
2473
- color: "var(--muted-foreground, #a1a1aa)",
2474
- fontFamily: "ui-monospace, SFMono-Regular, monospace",
2475
- fontSize: 12,
2476
- textDecoration: "none"
2477
- },
2525
+ style: buttonStyle(i),
2478
2526
  children: [
2479
2527
  /* @__PURE__ */ jsx(ProviderIcon, { id: p.id }),
2480
2528
  /* @__PURE__ */ jsx("span", { children: p.label })
@@ -2497,82 +2545,214 @@ function ProviderSignIn({
2497
2545
  }
2498
2546
  );
2499
2547
  }
2548
+ async function loadAdapter() {
2549
+ return await import('@orangecheck/wallet-adapter');
2550
+ }
2551
+ async function detectBrowserWallets() {
2552
+ return (await loadAdapter()).detectWallets().filter((w) => w.detected && w.id !== "manual").map((w) => ({ id: w.id, name: w.name }));
2553
+ }
2500
2554
  function WalletFlow({ authOrigin, audience, add, onSuccess }) {
2501
- const [address, setAddress] = React3.useState("");
2502
- const [error, setError] = React3.useState(null);
2503
- const [submitting, setSubmitting] = React3.useState(false);
2504
- const [stage, setStage] = React3.useState("enter");
2555
+ const uid = React4.useId();
2556
+ const ids = {
2557
+ address: `${uid}-address`,
2558
+ wallet: `${uid}-wallet`,
2559
+ message: `${uid}-message`,
2560
+ signature: `${uid}-signature`
2561
+ };
2562
+ const [address, setAddress] = React4.useState("");
2563
+ const [error, setError] = React4.useState(null);
2564
+ const [submitting, setSubmitting] = React4.useState(false);
2565
+ const [wallets, setWallets] = React4.useState(null);
2566
+ const [walletId, setWalletId] = React4.useState(null);
2567
+ const [manual, setManual] = React4.useState(null);
2568
+ const [pasted, setPasted] = React4.useState("");
2569
+ const [copied, setCopied] = React4.useState("idle");
2570
+ React4.useEffect(() => {
2571
+ let cancelled = false;
2572
+ detectBrowserWallets().then((found) => {
2573
+ if (cancelled) return;
2574
+ setWallets(found);
2575
+ setWalletId(found[0]?.id ?? null);
2576
+ }).catch(() => {
2577
+ if (!cancelled) setWallets([]);
2578
+ });
2579
+ return () => {
2580
+ cancelled = true;
2581
+ };
2582
+ }, []);
2583
+ async function fetchChallenge(addr) {
2584
+ const res = await fetch(
2585
+ `${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&audience=${encodeURIComponent(audience)}&purpose=login`
2586
+ );
2587
+ const json = await res.json();
2588
+ if (!res.ok || !json.message || !json.nonce) {
2589
+ throw new Error(json.error ?? "challenge_failed");
2590
+ }
2591
+ return { message: json.message, nonce: json.nonce };
2592
+ }
2593
+ async function submitSignature(message, nonce, signature, scheme) {
2594
+ const res = await fetch(`${authOrigin}/api/auth/signin`, {
2595
+ method: "POST",
2596
+ credentials: "include",
2597
+ headers: { "Content-Type": "application/json" },
2598
+ body: JSON.stringify({
2599
+ message,
2600
+ signature,
2601
+ ...scheme ? { scheme } : {},
2602
+ expectedNonce: nonce,
2603
+ expectedAudience: audience,
2604
+ expectedPurpose: "login",
2605
+ // Multi-account add-mode · the auth host preserves the current
2606
+ // roster_id when set, instead of minting a fresh one.
2607
+ ...add ? { add: true } : {}
2608
+ })
2609
+ });
2610
+ const json = await res.json();
2611
+ if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
2612
+ throw new Error(
2613
+ "reason" in json && json.reason || "error" in json && json.error || "verify_failed"
2614
+ );
2615
+ }
2616
+ onSuccess(json.account, json.token);
2617
+ }
2505
2618
  async function signIn(e) {
2506
2619
  e.preventDefault();
2507
2620
  const addr = address.trim();
2508
2621
  if (!addr) {
2509
- setError("paste a Bitcoin address");
2622
+ setError("Paste your Bitcoin address.");
2623
+ return;
2624
+ }
2625
+ if (!looksLikeBitcoinAddress(addr)) {
2626
+ setError(humanSigninError("bad_request", "wallet"));
2510
2627
  return;
2511
2628
  }
2512
2629
  setError(null);
2513
2630
  setSubmitting(true);
2514
- setStage("signing");
2631
+ let step = "challenge";
2515
2632
  try {
2516
- const challengeRes = await fetch(
2517
- `${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&audience=${encodeURIComponent(audience)}&purpose=login`
2518
- );
2519
- const challenge = await challengeRes.json();
2520
- if (!challengeRes.ok || !challenge.message || !challenge.nonce) {
2521
- throw new Error(challenge.error ?? "challenge_failed");
2633
+ let id = walletId;
2634
+ if (wallets === null) {
2635
+ const found = await detectBrowserWallets().catch(() => []);
2636
+ setWallets(found);
2637
+ id = found[0]?.id ?? null;
2638
+ setWalletId(id);
2522
2639
  }
2523
- let adapter;
2524
- try {
2525
- adapter = await import('@orangecheck/wallet-adapter');
2526
- } catch {
2527
- throw new Error(
2528
- "could not load wallet support \xB7 refresh to try again"
2529
- );
2530
- }
2531
- const wallets = adapter.detectWallets().filter((w) => w.detected && w.id !== "manual");
2532
- if (wallets.length === 0) {
2533
- throw new Error("no BIP-322 wallet extension detected \xB7 install one and refresh");
2640
+ const challenge = await fetchChallenge(addr);
2641
+ if (!id) {
2642
+ setManual(challenge);
2643
+ setSubmitting(false);
2644
+ return;
2534
2645
  }
2535
- const wallet = wallets[0];
2536
- const signer = adapter.getSigner(wallet.id, { address: addr });
2646
+ const signer = (await loadAdapter()).getSigner(id, { address: addr });
2537
2647
  const signature = await signer(challenge.message);
2538
- const res = await fetch(`${authOrigin}/api/auth/signin`, {
2539
- method: "POST",
2540
- credentials: "include",
2541
- headers: { "Content-Type": "application/json" },
2542
- body: JSON.stringify({
2543
- message: challenge.message,
2544
- signature,
2545
- scheme: "bip322",
2546
- expectedNonce: challenge.nonce,
2547
- expectedAudience: audience,
2548
- expectedPurpose: "login",
2549
- // Multi-account add-mode · the auth host preserves
2550
- // the current roster_id when set, instead of minting
2551
- // a fresh one. Hosts that haven't deployed the
2552
- // multi-account migration silently ignore the field.
2553
- ...add ? { add: true } : {}
2554
- })
2555
- });
2556
- const json = await res.json();
2557
- if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
2558
- throw new Error(
2559
- "reason" in json && json.reason || "error" in json && json.error || `verify_failed_${res.status}`
2560
- );
2561
- }
2562
- onSuccess(json.account, json.token);
2648
+ step = "submit";
2649
+ await submitSignature(challenge.message, challenge.nonce, signature, "bip322");
2563
2650
  } catch (err) {
2564
- const msg = err instanceof Error ? err.message : "sign-in failed";
2565
- setError(msg);
2651
+ setError(failureMessage(err, "wallet", step));
2566
2652
  setSubmitting(false);
2567
- setStage("enter");
2568
2653
  }
2569
2654
  }
2655
+ async function signInManual(e) {
2656
+ e.preventDefault();
2657
+ if (!manual) return;
2658
+ const sig = pasted.trim();
2659
+ if (!sig) {
2660
+ setError("Paste the signature your wallet gave you.");
2661
+ return;
2662
+ }
2663
+ setError(null);
2664
+ setSubmitting(true);
2665
+ try {
2666
+ await submitSignature(manual.message, manual.nonce, sig, void 0);
2667
+ } catch (err) {
2668
+ setError(failureMessage(err, "wallet", "submit"));
2669
+ setSubmitting(false);
2670
+ }
2671
+ }
2672
+ async function copyMessage() {
2673
+ if (!manual) return;
2674
+ try {
2675
+ await navigator.clipboard.writeText(manual.message);
2676
+ setCopied("copied");
2677
+ } catch {
2678
+ setCopied("failed");
2679
+ }
2680
+ }
2681
+ if (manual) {
2682
+ return /* @__PURE__ */ jsxs("form", { onSubmit: signInManual, "data-oc-signin-wallet-manual": "", children: [
2683
+ /* @__PURE__ */ jsxs(FlowHeader, { children: [
2684
+ "In your wallet, use \u201Csign message\u201D (BIP-322) with ",
2685
+ address.trim(),
2686
+ ", sign the text below, and paste the signature back here. Nothing is spent."
2687
+ ] }),
2688
+ /* @__PURE__ */ jsx(Label, { htmlFor: ids.message, children: "message to sign" }),
2689
+ /* @__PURE__ */ jsx(
2690
+ "textarea",
2691
+ {
2692
+ id: ids.message,
2693
+ readOnly: true,
2694
+ value: manual.message,
2695
+ rows: 5,
2696
+ onFocus: (e) => e.currentTarget.select(),
2697
+ style: { ...inputStyle2, resize: "vertical" }
2698
+ }
2699
+ ),
2700
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void copyMessage(), style: linkButtonStyle, children: copied === "copied" ? "copied" : "copy message" }),
2701
+ copied === "failed" && /* @__PURE__ */ jsx(Hint, { children: "Couldn't copy here. Select the text above and copy it." }),
2702
+ /* @__PURE__ */ jsxs("div", { style: { marginTop: 14 }, children: [
2703
+ /* @__PURE__ */ jsx(Label, { htmlFor: ids.signature, children: "signature" }),
2704
+ /* @__PURE__ */ jsx(
2705
+ "textarea",
2706
+ {
2707
+ id: ids.signature,
2708
+ value: pasted,
2709
+ onChange: (e) => {
2710
+ setPasted(e.target.value);
2711
+ if (error) setError(null);
2712
+ },
2713
+ rows: 3,
2714
+ placeholder: "paste the signature",
2715
+ spellCheck: false,
2716
+ autoFocus: true,
2717
+ "aria-invalid": error ? true : void 0,
2718
+ style: { ...inputStyle2, resize: "vertical" }
2719
+ }
2720
+ )
2721
+ ] }),
2722
+ error && /* @__PURE__ */ jsx(ErrorLine2, { children: error }),
2723
+ /* @__PURE__ */ jsx(
2724
+ "button",
2725
+ {
2726
+ type: "submit",
2727
+ disabled: submitting || !pasted.trim(),
2728
+ style: submitStyle(submitting || !pasted.trim()),
2729
+ children: submitting ? "checking\u2026" : "sign me in \u2192"
2730
+ }
2731
+ ),
2732
+ /* @__PURE__ */ jsx(
2733
+ "button",
2734
+ {
2735
+ type: "button",
2736
+ onClick: () => {
2737
+ setManual(null);
2738
+ setPasted("");
2739
+ setError(null);
2740
+ setCopied("idle");
2741
+ },
2742
+ style: linkButtonStyle,
2743
+ children: "use a different address"
2744
+ }
2745
+ )
2746
+ ] });
2747
+ }
2748
+ const noBrowserWallet = wallets !== null && wallets.length === 0;
2570
2749
  return /* @__PURE__ */ jsxs("form", { onSubmit: signIn, "data-oc-signin-wallet": "", children: [
2571
- /* @__PURE__ */ jsx(FlowHeader, { label: "\xA7 bitcoin wallet", children: "Sign in with any BIP-322-capable Bitcoin wallet (Sparrow, Xverse, Leather, UniSat, Alby, OKX, Phantom). Paste your address, click sign \u2014 your wallet extension prompts for a one-time signature on a short challenge. The address becomes your OC identity. Private key material never leaves your wallet." }),
2572
- /* @__PURE__ */ jsx(Label, { children: "bitcoin address" }),
2750
+ /* @__PURE__ */ jsx(FlowHeader, { children: "Your wallet signs a short message to prove you hold the address. Nothing is spent and your key never leaves the wallet. Works with browser wallets, or any wallet that can sign a message, like Sparrow." }),
2751
+ /* @__PURE__ */ jsx(Label, { htmlFor: ids.address, children: "bitcoin address" }),
2573
2752
  /* @__PURE__ */ jsx(
2574
2753
  "input",
2575
2754
  {
2755
+ id: ids.address,
2576
2756
  type: "text",
2577
2757
  value: address,
2578
2758
  onChange: (e) => {
@@ -2588,6 +2768,19 @@ function WalletFlow({ authOrigin, audience, add, onSuccess }) {
2588
2768
  style: inputStyle2
2589
2769
  }
2590
2770
  ),
2771
+ wallets && wallets.length > 1 && /* @__PURE__ */ jsxs("div", { style: { marginTop: 12 }, children: [
2772
+ /* @__PURE__ */ jsx(Label, { htmlFor: ids.wallet, children: "sign with" }),
2773
+ /* @__PURE__ */ jsx(
2774
+ "select",
2775
+ {
2776
+ id: ids.wallet,
2777
+ value: walletId ?? "",
2778
+ onChange: (e) => setWalletId(e.target.value),
2779
+ style: inputStyle2,
2780
+ children: wallets.map((w) => /* @__PURE__ */ jsx("option", { value: w.id, children: w.name }, w.id))
2781
+ }
2782
+ )
2783
+ ] }),
2591
2784
  error && /* @__PURE__ */ jsx(ErrorLine2, { children: error }),
2592
2785
  /* @__PURE__ */ jsx(
2593
2786
  "button",
@@ -2595,54 +2788,71 @@ function WalletFlow({ authOrigin, audience, add, onSuccess }) {
2595
2788
  type: "submit",
2596
2789
  disabled: submitting || !address.trim(),
2597
2790
  style: submitStyle(submitting || !address.trim()),
2598
- children: stage === "signing" && submitting ? "waiting for wallet\u2026" : "sign challenge \xB7 sign me in \u2192"
2791
+ children: submitting ? "waiting for wallet\u2026" : noBrowserWallet ? "get the message to sign \u2192" : "sign in with wallet \u2192"
2599
2792
  }
2600
2793
  ),
2601
- /* @__PURE__ */ jsx(Hint, { children: "Detection picks the first installed BIP-322-capable extension. Address is the one your wallet will sign for." })
2794
+ noBrowserWallet && /* @__PURE__ */ jsx(Hint, { children: "No browser wallet found here. You can sign with any wallet and paste." })
2602
2795
  ] });
2603
2796
  }
2604
2797
  function EmailFlow({ authOrigin, add, onSuccess }) {
2605
- const [stage, setStage] = React3.useState("enter");
2606
- const [email, setEmail] = React3.useState("");
2607
- const [emailError, setEmailError] = React3.useState(null);
2608
- const [code, setCode] = React3.useState("");
2609
- const [codeError, setCodeError] = React3.useState(null);
2610
- const [token, setToken] = React3.useState(null);
2611
- const [submitting, setSubmitting] = React3.useState(false);
2612
- async function start(e) {
2613
- e.preventDefault();
2614
- const trimmed = email.trim().toLowerCase();
2615
- if (!trimmed || !/.+@.+\..+/.test(trimmed)) {
2616
- setEmailError("enter a valid email");
2617
- return;
2618
- }
2619
- setEmailError(null);
2620
- setSubmitting(true);
2798
+ const uid = React4.useId();
2799
+ const [stage, setStage] = React4.useState("enter");
2800
+ const [email, setEmail] = React4.useState("");
2801
+ const [emailError, setEmailError] = React4.useState(null);
2802
+ const [code, setCode] = React4.useState("");
2803
+ const [codeError, setCodeError] = React4.useState(null);
2804
+ const [token, setToken] = React4.useState(null);
2805
+ const [submitting, setSubmitting] = React4.useState(false);
2806
+ const [resent, setResent] = React4.useState(false);
2807
+ async function sendCode(address) {
2621
2808
  try {
2622
2809
  const res = await fetch(`${authOrigin}/api/auth/email-otp/start`, {
2623
2810
  method: "POST",
2624
2811
  credentials: "include",
2625
2812
  headers: { "Content-Type": "application/json" },
2626
- body: JSON.stringify({ email: trimmed })
2813
+ body: JSON.stringify({ email: address })
2627
2814
  });
2628
2815
  const json = await res.json();
2629
- if (!res.ok || !json.token) {
2630
- throw new Error(json.reason ?? `start failed (${res.status})`);
2631
- }
2816
+ if (!res.ok || !json.token) return humanSigninError(json.reason ?? "server_error", "email");
2632
2817
  setToken(json.token);
2633
- setEmail(trimmed);
2634
- setStage("code");
2635
- } catch (err) {
2636
- setEmailError(err instanceof Error ? err.message : "failed to send code");
2637
- } finally {
2638
- setSubmitting(false);
2818
+ return null;
2819
+ } catch {
2820
+ return NETWORK_ERROR;
2821
+ }
2822
+ }
2823
+ async function start(e) {
2824
+ e.preventDefault();
2825
+ const trimmed = email.trim().toLowerCase();
2826
+ if (!trimmed || !/.+@.+\..+/.test(trimmed)) {
2827
+ setEmailError(humanSigninError("invalid_email", "email"));
2828
+ return;
2829
+ }
2830
+ setEmailError(null);
2831
+ setSubmitting(true);
2832
+ const err = await sendCode(trimmed);
2833
+ setSubmitting(false);
2834
+ if (err) {
2835
+ setEmailError(err);
2836
+ return;
2639
2837
  }
2838
+ setEmail(trimmed);
2839
+ setStage("code");
2840
+ }
2841
+ async function resend() {
2842
+ setCodeError(null);
2843
+ setResent(false);
2844
+ setSubmitting(true);
2845
+ const err = await sendCode(email);
2846
+ setSubmitting(false);
2847
+ setCode("");
2848
+ if (err) setCodeError(err);
2849
+ else setResent(true);
2640
2850
  }
2641
2851
  async function verify(e) {
2642
2852
  e.preventDefault();
2643
2853
  if (!token) return;
2644
2854
  if (code.length !== 6) {
2645
- setCodeError("6 digits");
2855
+ setCodeError("The code is 6 digits.");
2646
2856
  return;
2647
2857
  }
2648
2858
  setCodeError(null);
@@ -2663,23 +2873,24 @@ function EmailFlow({ authOrigin, add, onSuccess }) {
2663
2873
  const json = await res.json();
2664
2874
  if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
2665
2875
  throw new Error(
2666
- "reason" in json && json.reason || "error" in json && json.error || `verify failed (${res.status})`
2876
+ "reason" in json && json.reason || "error" in json && json.error || "server_error"
2667
2877
  );
2668
2878
  }
2669
2879
  onSuccess(json.account, json.token);
2670
2880
  } catch (err) {
2671
- setCodeError(err instanceof Error ? err.message : "verify failed");
2881
+ setCodeError(failureMessage(err, "email", "submit"));
2672
2882
  } finally {
2673
2883
  setSubmitting(false);
2674
2884
  }
2675
2885
  }
2676
2886
  if (stage === "enter") {
2677
2887
  return /* @__PURE__ */ jsxs("form", { onSubmit: start, "data-oc-signin-email": "", children: [
2678
- /* @__PURE__ */ jsx(FlowHeader, { label: "\xA7 email + otp", children: "We email you a 6-digit code. No password, and no wallet needed to start. Link a Bitcoin address any time \u2014 that is the identity you hold yourself." }),
2679
- /* @__PURE__ */ jsx(Label, { children: "email" }),
2888
+ /* @__PURE__ */ jsx(FlowHeader, { children: "We email you a 6-digit code. No password, and no wallet needed to start. Link a Bitcoin address any time \u2014 that is the identity you hold yourself." }),
2889
+ /* @__PURE__ */ jsx(Label, { htmlFor: `${uid}-email`, children: "email" }),
2680
2890
  /* @__PURE__ */ jsx(
2681
2891
  "input",
2682
2892
  {
2893
+ id: `${uid}-email`,
2683
2894
  type: "email",
2684
2895
  autoComplete: "email",
2685
2896
  value: email,
@@ -2694,20 +2905,20 @@ function EmailFlow({ authOrigin, add, onSuccess }) {
2694
2905
  }
2695
2906
  ),
2696
2907
  emailError && /* @__PURE__ */ jsx(ErrorLine2, { children: emailError }),
2697
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: submitting, style: submitStyle(submitting), children: submitting ? "sending\u2026" : "send one-time code \u2192" }),
2698
- /* @__PURE__ */ jsx(Hint, { children: "Codes come from the auth host, expire in 10 minutes, and work once." })
2908
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: submitting, style: submitStyle(submitting), children: submitting ? "sending\u2026" : "send one-time code \u2192" })
2699
2909
  ] });
2700
2910
  }
2701
2911
  return /* @__PURE__ */ jsxs("form", { onSubmit: verify, "data-oc-signin-email-verify": "", children: [
2702
- /* @__PURE__ */ jsxs(FlowHeader, { label: "\xA7 enter the code", children: [
2703
- "Sent a 6-digit code to ",
2912
+ /* @__PURE__ */ jsxs(FlowHeader, { children: [
2913
+ resent ? "Sent a new code to " : "Sent a 6-digit code to ",
2704
2914
  /* @__PURE__ */ jsx("span", { style: { color: "var(--foreground, #fafafa)" }, children: email }),
2705
- ". Code expires in 10 minutes."
2915
+ ". It expires in 10 minutes."
2706
2916
  ] }),
2707
- /* @__PURE__ */ jsx(Label, { children: "one-time code" }),
2917
+ /* @__PURE__ */ jsx(Label, { htmlFor: `${uid}-code`, children: "one-time code" }),
2708
2918
  /* @__PURE__ */ jsx(
2709
2919
  "input",
2710
2920
  {
2921
+ id: `${uid}-code`,
2711
2922
  type: "text",
2712
2923
  inputMode: "numeric",
2713
2924
  pattern: "\\d{6}",
@@ -2735,31 +2946,32 @@ function EmailFlow({ authOrigin, add, onSuccess }) {
2735
2946
  children: submitting ? "verifying\u2026" : "verify \xB7 sign me in \u2192"
2736
2947
  }
2737
2948
  ),
2738
- /* @__PURE__ */ jsx(
2739
- "button",
2740
- {
2741
- type: "button",
2742
- onClick: () => {
2743
- setStage("enter");
2744
- setCode("");
2745
- setToken(null);
2746
- },
2747
- style: {
2748
- marginTop: 12,
2749
- background: "transparent",
2750
- border: "none",
2751
- color: "var(--muted-foreground, #a1a1aa)",
2752
- fontFamily: "ui-monospace, SFMono-Regular, monospace",
2753
- fontSize: 11,
2754
- letterSpacing: "0.12em",
2755
- textTransform: "uppercase",
2756
- cursor: "pointer",
2757
- padding: 0
2758
- },
2759
- children: "use a different email"
2760
- }
2761
- ),
2762
- /* @__PURE__ */ jsx(Hint, { children: "On verify, oc_session is set on Domain=.ochk.io family-wide." })
2949
+ /* @__PURE__ */ jsxs("div", { children: [
2950
+ /* @__PURE__ */ jsx(
2951
+ "button",
2952
+ {
2953
+ type: "button",
2954
+ onClick: () => void resend(),
2955
+ disabled: submitting,
2956
+ style: linkButtonStyle,
2957
+ children: "send a new code"
2958
+ }
2959
+ ),
2960
+ /* @__PURE__ */ jsx(
2961
+ "button",
2962
+ {
2963
+ type: "button",
2964
+ onClick: () => {
2965
+ setStage("enter");
2966
+ setCode("");
2967
+ setToken(null);
2968
+ setResent(false);
2969
+ },
2970
+ style: linkButtonStyle,
2971
+ children: "use a different email"
2972
+ }
2973
+ )
2974
+ ] })
2763
2975
  ] });
2764
2976
  }
2765
2977
  function FlowHeader({
@@ -2767,7 +2979,7 @@ function FlowHeader({
2767
2979
  children
2768
2980
  }) {
2769
2981
  return /* @__PURE__ */ jsxs("div", { style: { marginBottom: 16 }, children: [
2770
- /* @__PURE__ */ jsx(
2982
+ label && /* @__PURE__ */ jsx(
2771
2983
  "div",
2772
2984
  {
2773
2985
  style: {
@@ -2795,10 +3007,30 @@ function FlowHeader({
2795
3007
  )
2796
3008
  ] });
2797
3009
  }
2798
- function Label({ children }) {
3010
+ var linkButtonStyle = {
3011
+ display: "inline-flex",
3012
+ alignItems: "center",
3013
+ minHeight: 44,
3014
+ marginTop: 4,
3015
+ marginRight: 16,
3016
+ background: "transparent",
3017
+ border: "none",
3018
+ color: "var(--muted-foreground, #a1a1aa)",
3019
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
3020
+ fontSize: 11,
3021
+ letterSpacing: "0.12em",
3022
+ textTransform: "uppercase",
3023
+ cursor: "pointer",
3024
+ padding: 0
3025
+ };
3026
+ function Label({
3027
+ htmlFor,
3028
+ children
3029
+ }) {
2799
3030
  return /* @__PURE__ */ jsx(
2800
3031
  "label",
2801
3032
  {
3033
+ htmlFor,
2802
3034
  style: {
2803
3035
  display: "block",
2804
3036
  color: "var(--foreground, #fafafa)",
@@ -2828,7 +3060,9 @@ var inputStyle2 = {
2828
3060
  };
2829
3061
  var linkAlsoStyle = {
2830
3062
  display: "flex",
2831
- alignItems: "flex-start",
3063
+ alignItems: "center",
3064
+ // The whole row is the checkbox's hit area; the 13px box alone was not.
3065
+ minHeight: 44,
2832
3066
  gap: 8,
2833
3067
  marginTop: 16,
2834
3068
  paddingTop: 14,
@@ -2884,8 +3118,7 @@ function Hint({ children }) {
2884
3118
  color: "var(--muted-foreground, #a1a1aa)",
2885
3119
  fontFamily: "ui-monospace, SFMono-Regular, monospace",
2886
3120
  fontSize: 10.5,
2887
- lineHeight: 1.55,
2888
- opacity: 0.7
3121
+ lineHeight: 1.55
2889
3122
  },
2890
3123
  children: [
2891
3124
  "> ",
@@ -2904,13 +3137,13 @@ function isAbortError(err) {
2904
3137
  }
2905
3138
  function useWebAuthnRegister(opts) {
2906
3139
  const authOrigin = resolveAuthOrigin(opts);
2907
- const [status, setStatus] = React3.useState("idle");
2908
- const [error, setError] = React3.useState(null);
2909
- const reset = React3.useCallback(() => {
3140
+ const [status, setStatus] = React4.useState("idle");
3141
+ const [error, setError] = React4.useState(null);
3142
+ const reset = React4.useCallback(() => {
2910
3143
  setStatus("idle");
2911
3144
  setError(null);
2912
3145
  }, []);
2913
- const register = React3.useCallback(
3146
+ const register = React4.useCallback(
2914
3147
  async (args) => {
2915
3148
  setError(null);
2916
3149
  setStatus("requesting-options");
@@ -2972,10 +3205,10 @@ function useWebAuthnRegister(opts) {
2972
3205
  function useWebAuthnList(opts) {
2973
3206
  const authOrigin = resolveAuthOrigin(opts);
2974
3207
  const { status: sessionStatus } = useOcSession();
2975
- const [credentials, setCredentials] = React3.useState([]);
2976
- const [status, setStatus] = React3.useState("loading");
2977
- const [error, setError] = React3.useState(null);
2978
- const refetch = React3.useCallback(async () => {
3208
+ const [credentials, setCredentials] = React4.useState([]);
3209
+ const [status, setStatus] = React4.useState("loading");
3210
+ const [error, setError] = React4.useState(null);
3211
+ const refetch = React4.useCallback(async () => {
2979
3212
  setError(null);
2980
3213
  if (sessionStatus !== "authenticated") {
2981
3214
  setCredentials([]);
@@ -3003,10 +3236,10 @@ function useWebAuthnList(opts) {
3003
3236
  setStatus("error");
3004
3237
  }
3005
3238
  }, [authOrigin, sessionStatus]);
3006
- React3.useEffect(() => {
3239
+ React4.useEffect(() => {
3007
3240
  void refetch();
3008
3241
  }, [refetch]);
3009
- const rename = React3.useCallback(
3242
+ const rename = React4.useCallback(
3010
3243
  async (id, label) => {
3011
3244
  try {
3012
3245
  const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
@@ -3031,7 +3264,7 @@ function useWebAuthnList(opts) {
3031
3264
  },
3032
3265
  [authOrigin, refetch]
3033
3266
  );
3034
- const remove = React3.useCallback(
3267
+ const remove = React4.useCallback(
3035
3268
  async (id) => {
3036
3269
  try {
3037
3270
  const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
@@ -3059,13 +3292,13 @@ function useWebAuthnList(opts) {
3059
3292
  function useStepUpAuth(opts) {
3060
3293
  const authOrigin = resolveAuthOrigin(opts);
3061
3294
  const { refresh } = useOcSession();
3062
- const [status, setStatus] = React3.useState("idle");
3063
- const [error, setError] = React3.useState(null);
3064
- const reset = React3.useCallback(() => {
3295
+ const [status, setStatus] = React4.useState("idle");
3296
+ const [error, setError] = React4.useState(null);
3297
+ const reset = React4.useCallback(() => {
3065
3298
  setStatus("idle");
3066
3299
  setError(null);
3067
3300
  }, []);
3068
- const stepUp = React3.useCallback(
3301
+ const stepUp = React4.useCallback(
3069
3302
  async (args) => {
3070
3303
  setError(null);
3071
3304
  setStatus("requesting-options");