@orangecheck/auth-client 2.33.4 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/appearance.mjs +23 -0
  2. package/dist/appearance.mjs.map +1 -0
  3. package/dist/bond.mjs +20 -0
  4. package/dist/bond.mjs.map +1 -0
  5. package/dist/chunk-4E7GJ5IB.mjs +1184 -0
  6. package/dist/chunk-4E7GJ5IB.mjs.map +1 -0
  7. package/dist/chunk-7ST4Q5KR.mjs +346 -0
  8. package/dist/chunk-7ST4Q5KR.mjs.map +1 -0
  9. package/dist/chunk-7UYPMTAV.mjs +652 -0
  10. package/dist/chunk-7UYPMTAV.mjs.map +1 -0
  11. package/dist/chunk-BYAF2P2L.mjs +253 -0
  12. package/dist/chunk-BYAF2P2L.mjs.map +1 -0
  13. package/dist/chunk-CYR7J4UY.mjs +118 -0
  14. package/dist/chunk-CYR7J4UY.mjs.map +1 -0
  15. package/dist/chunk-H2TXOCXH.mjs +23 -0
  16. package/dist/chunk-H2TXOCXH.mjs.map +1 -0
  17. package/dist/chunk-HZ4FJMF3.mjs +490 -0
  18. package/dist/chunk-HZ4FJMF3.mjs.map +1 -0
  19. package/dist/chunk-TJJNTITX.mjs +187 -0
  20. package/dist/chunk-TJJNTITX.mjs.map +1 -0
  21. package/dist/chunk-UFRU3I3D.mjs +913 -0
  22. package/dist/chunk-UFRU3I3D.mjs.map +1 -0
  23. package/dist/chunk-VRLFZAGK.mjs +37 -0
  24. package/dist/chunk-VRLFZAGK.mjs.map +1 -0
  25. package/dist/chunk-XCZVSE7M.mjs +40 -0
  26. package/dist/chunk-XCZVSE7M.mjs.map +1 -0
  27. package/dist/components.mjs +19 -0
  28. package/dist/components.mjs.map +1 -0
  29. package/dist/index.js +77 -67
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +112 -4001
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/linked-identities.mjs +41 -0
  34. package/dist/linked-identities.mjs.map +1 -0
  35. package/dist/provider.mjs +17 -0
  36. package/dist/provider.mjs.map +1 -0
  37. package/dist/return-to.mjs +18 -0
  38. package/dist/return-to.mjs.map +1 -0
  39. package/dist/signin.mjs +23 -0
  40. package/dist/signin.mjs.map +1 -0
  41. package/dist/sudo.mjs +16 -0
  42. package/dist/sudo.mjs.map +1 -0
  43. package/dist/tab-session.mjs +29 -0
  44. package/dist/tab-session.mjs.map +1 -0
  45. package/dist/types.mjs +17 -0
  46. package/dist/types.mjs.map +1 -0
  47. package/dist/webauthn.mjs +15 -0
  48. package/dist/webauthn.mjs.map +1 -0
  49. package/package.json +2 -1
  50. package/dist/index.d.mts +0 -303
package/dist/index.mjs CHANGED
@@ -1,4003 +1,114 @@
1
- import * as React4 from 'react';
2
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
- import { startRegistration, startAuthentication } from '@simplewebauthn/browser';
4
-
5
- // src/provider.tsx
6
-
7
- // src/tab-session.ts
8
- var TAB_SESSION_HEADER = "x-oc-tab-session";
9
- var TAB_SESSION_STORAGE_KEY = "oc_tab_session";
10
- var TAB_ACCOUNT_HINT_KEY = "oc-as";
11
- var TAB_ADOPT_HASH = "#oc-adopt";
12
- function readTabSession() {
13
- if (typeof window === "undefined") return null;
14
- try {
15
- const raw = window.sessionStorage.getItem(TAB_SESSION_STORAGE_KEY);
16
- if (!raw) return null;
17
- const parsed = JSON.parse(raw);
18
- if (typeof parsed.token !== "string" || parsed.token.length === 0) return null;
19
- if (typeof parsed.didOc !== "string" || parsed.didOc.length === 0) return null;
20
- return { token: parsed.token, didOc: parsed.didOc };
21
- } catch {
22
- return null;
23
- }
24
- }
25
- function writeTabSession(session) {
26
- if (typeof window === "undefined") return;
27
- try {
28
- window.sessionStorage.setItem(TAB_SESSION_STORAGE_KEY, JSON.stringify(session));
29
- } catch {
30
- }
31
- }
32
- function clearTabSession() {
33
- if (typeof window === "undefined") return;
34
- try {
35
- window.sessionStorage.removeItem(TAB_SESSION_STORAGE_KEY);
36
- } catch {
37
- }
38
- }
39
- function tabSessionHeader() {
40
- const pin = readTabSession();
41
- return pin ? { [TAB_SESSION_HEADER]: pin.token } : {};
42
- }
43
- function isPinnableUrl(url, authOrigin) {
44
- if (typeof window === "undefined") return false;
45
- try {
46
- const u = new URL(url, window.location.href);
47
- return u.origin === window.location.origin || u.origin === authOrigin;
48
- } catch {
49
- return false;
50
- }
51
- }
52
- function installTabFetchInterceptor(authOrigin) {
53
- if (typeof window === "undefined") return () => {
54
- };
55
- const original = window.fetch;
56
- const wrapped = (input, init) => {
57
- try {
58
- const pin = readTabSession();
59
- if (pin) {
60
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
61
- if (isPinnableUrl(url, authOrigin)) {
62
- const headers = new Headers(
63
- init?.headers ?? (input instanceof Request ? input.headers : void 0)
64
- );
65
- if (!headers.has("authorization") && !headers.has(TAB_SESSION_HEADER)) {
66
- headers.set(TAB_SESSION_HEADER, pin.token);
67
- init = { ...init, headers };
68
- }
69
- }
70
- }
71
- } catch {
72
- }
73
- return original.call(window, input, init);
74
- };
75
- window.fetch = wrapped;
76
- return () => {
77
- if (window.fetch === wrapped) window.fetch = original;
78
- };
79
- }
80
- function consumeTabAdoptMarker() {
81
- if (typeof window === "undefined") return false;
82
- if (!window.location.hash.includes(TAB_ADOPT_HASH.slice(1))) return false;
83
- clearTabSession();
84
- try {
85
- const url = new URL(window.location.href);
86
- url.hash = "";
87
- window.history.replaceState(window.history.state, "", url.toString());
88
- } catch {
89
- }
90
- return true;
91
- }
92
- var OC_AS_HINT_RE = /^oc-as=(did:oc:[0-9a-f]{32})$/;
93
- function familyApex(authOrigin) {
94
- try {
95
- return new URL(authOrigin).hostname.toLowerCase();
96
- } catch {
97
- return null;
98
- }
99
- }
100
- function isFamilyUrl(u, authOrigin) {
101
- if (u.protocol !== "http:" && u.protocol !== "https:") return false;
102
- if (typeof window !== "undefined" && u.origin === window.location.origin) return true;
103
- const apex = familyApex(authOrigin);
104
- if (!apex) return false;
105
- const host = u.hostname.toLowerCase();
106
- return host === apex || host.endsWith(`.${apex}`);
107
- }
108
- function opensWithoutTabPin(e, anchor, dest) {
109
- if (typeof window !== "undefined" && dest.origin !== window.location.origin) return true;
110
- const target = (anchor.getAttribute("target") ?? "").toLowerCase();
111
- return e.button === 1 || // middle-click
112
- e.metaKey || e.ctrlKey || e.shiftKey || target === "_blank";
113
- }
114
- function installTabLinkDecorator(authOrigin) {
115
- if (typeof window === "undefined" || typeof document === "undefined") return () => {
116
- };
117
- const onActivate = (e) => {
118
- try {
119
- const pin = readTabSession();
120
- if (!pin) return;
121
- const target = e.target;
122
- if (!(target instanceof Element)) return;
123
- const anchor = target.closest("a[href]");
124
- if (!anchor || anchor.hasAttribute("download")) return;
125
- const raw = anchor.getAttribute("href");
126
- if (!raw || raw.includes(`${TAB_ACCOUNT_HINT_KEY}=`)) return;
127
- let dest;
128
- try {
129
- dest = new URL(anchor.href);
130
- } catch {
131
- return;
132
- }
133
- if (dest.hash) return;
134
- if (!isFamilyUrl(dest, authOrigin)) return;
135
- if (!opensWithoutTabPin(e, anchor, dest)) return;
136
- anchor.setAttribute("href", `${raw}#${TAB_ACCOUNT_HINT_KEY}=${pin.didOc}`);
137
- } catch {
138
- }
139
- };
140
- window.addEventListener("click", onActivate, true);
141
- window.addEventListener("auxclick", onActivate, true);
142
- return () => {
143
- window.removeEventListener("click", onActivate, true);
144
- window.removeEventListener("auxclick", onActivate, true);
145
- };
146
- }
147
- async function consumeTabAccountHint(authOrigin) {
148
- if (typeof window === "undefined") return null;
149
- const frag = window.location.hash.replace(/^#/, "");
150
- const m = OC_AS_HINT_RE.exec(frag);
151
- if (!m) return null;
152
- const did = m[1];
153
- try {
154
- const url = new URL(window.location.href);
155
- url.hash = "";
156
- window.history.replaceState(window.history.state, "", url.toString());
157
- } catch {
158
- }
159
- const existing = readTabSession();
160
- if (existing && existing.didOc === did) return did;
161
- try {
162
- const res = await fetch(`${authOrigin}/api/auth/tab`, {
163
- method: "POST",
164
- credentials: "include",
165
- headers: { Accept: "application/json", "Content-Type": "application/json" },
166
- body: JSON.stringify({ did_oc: did })
167
- });
168
- if (!res.ok) return null;
169
- const body = await res.json();
170
- if (body.ok && typeof body.token === "string" && body.account?.did_oc === did) {
171
- writeTabSession({ token: body.token, didOc: did });
172
- return did;
173
- }
174
- } catch {
175
- }
176
- return null;
177
- }
178
-
179
- // src/return-to.ts
180
- var PREFIX = "oc:return-fragment:";
181
- var HOLD_MS = 1e4;
182
- var FRAGMENT_TTL_MS = 15 * 60 * 1e3;
183
- function store() {
184
- try {
185
- return window.sessionStorage;
186
- } catch {
187
- return null;
188
- }
189
- }
190
- function stripFragment(target) {
191
- const cut = target.indexOf("#");
192
- return cut < 0 ? target : target.slice(0, cut);
193
- }
194
- function keep(pathAndQuery, hash) {
195
- if (hash.length < 2) return;
196
- try {
197
- store()?.setItem(PREFIX + pathAndQuery, JSON.stringify({ hash, at: Date.now() }));
198
- } catch {
199
- }
200
- }
201
- function withoutFragment(target) {
202
- if (typeof window !== "undefined" && target.includes("#")) {
203
- try {
204
- const u = new URL(target, window.location.href);
205
- if (u.origin === window.location.origin) keep(u.pathname + u.search, u.hash);
206
- } catch {
207
- }
208
- }
209
- return stripFragment(target);
210
- }
211
- function returnToHere() {
212
- if (typeof window === "undefined") return "/";
213
- const { pathname, search, hash } = window.location;
214
- return withoutFragment(pathname + search + hash);
215
- }
216
- function keepFragmentOnReturnLinks() {
217
- if (typeof document === "undefined") return () => {
218
- };
219
- const onClick = (e) => {
220
- const { pathname, search, hash, href } = window.location;
221
- if (hash.length < 2 || e.defaultPrevented) return;
222
- const link = e.target?.closest?.("a[href]");
223
- if (!link) return;
224
- try {
225
- const to = new URL(link.getAttribute("href") ?? "", href);
226
- const back = to.searchParams.get("return_to");
227
- if (!back) return;
228
- const here = new URL(back, href);
229
- if (here.origin === window.location.origin && here.pathname === pathname && here.search === search) {
230
- keep(pathname + search, hash);
231
- }
232
- } catch {
233
- }
234
- };
235
- document.addEventListener("click", onClick, true);
236
- return () => document.removeEventListener("click", onClick, true);
237
- }
238
- function restoreReturnFragment() {
239
- if (typeof window === "undefined") return false;
240
- const s = store();
241
- if (!s) return false;
242
- const { pathname, search, hash } = window.location;
243
- const key = PREFIX + pathname + search;
244
- let raw;
245
- try {
246
- raw = s.getItem(key);
247
- if (raw === null) return false;
248
- s.removeItem(key);
249
- } catch {
250
- return false;
251
- }
252
- if (hash) return false;
253
- try {
254
- const kept = JSON.parse(raw);
255
- if (typeof kept.hash !== "string" || !kept.hash.startsWith("#")) return false;
256
- if (typeof kept.at !== "number" || Date.now() - kept.at > FRAGMENT_TTL_MS) return false;
257
- const restored = pathname + search + kept.hash;
258
- window.history.replaceState(window.history.state, "", restored);
259
- holdFragment(pathname + search, kept.hash, restored);
260
- return true;
261
- } catch {
262
- return false;
263
- }
264
- }
265
- function holdFragment(pathAndQuery, hash, restored) {
266
- const until = Date.now() + HOLD_MS;
267
- const check = () => {
268
- const l = window.location;
269
- if (l.pathname + l.search !== pathAndQuery || l.hash === hash) {
270
- if (l.pathname + l.search === pathAndQuery && Date.now() < until) {
271
- window.setTimeout(check, 50);
272
- }
273
- return;
274
- }
275
- if (l.hash && l.hash !== TAB_ADOPT_HASH) return;
276
- try {
277
- window.history.replaceState(window.history.state, "", restored);
278
- } catch {
279
- }
280
- };
281
- window.setTimeout(check, 0);
282
- }
283
-
284
- // src/types.ts
285
- var DISPLAY_IDENTITY_KINDS = ["did", "btc", "email", "nostr"];
286
- var DEFAULT_CONFIG = {
287
- authOrigin: "https://ochk.io",
288
- signInPath: "/signin",
289
- mePath: "/api/auth/me",
290
- logoutPath: "/api/auth/logout",
291
- multiAccount: true
1
+ import {
2
+ useStepUpAuth,
3
+ useWebAuthnList,
4
+ useWebAuthnRegister
5
+ } from "./chunk-BYAF2P2L.mjs";
6
+ import {
7
+ OcBond,
8
+ bondFloorsText,
9
+ fetchOcBond
10
+ } from "./chunk-HZ4FJMF3.mjs";
11
+ import {
12
+ OcAccountChip,
13
+ OcAccountPill,
14
+ OcAddressInput,
15
+ OcSignInButton,
16
+ useOcAddressSuggestion
17
+ } from "./chunk-7UYPMTAV.mjs";
18
+ import {
19
+ OcSessionProvider,
20
+ useOcSession,
21
+ useOptionalOcSession
22
+ } from "./chunk-7ST4Q5KR.mjs";
23
+ import {
24
+ OcSignIn,
25
+ familyReturnTarget,
26
+ safeReturnTo
27
+ } from "./chunk-4E7GJ5IB.mjs";
28
+ import {
29
+ OcLinkedIdentities,
30
+ fetchOcLinkedIdentities
31
+ } from "./chunk-UFRU3I3D.mjs";
32
+ import {
33
+ SUDO_ACCOUNT_MISMATCH_MESSAGE,
34
+ handleSudoRequired,
35
+ isSudoAccountMismatch,
36
+ redirectToSudo
37
+ } from "./chunk-XCZVSE7M.mjs";
38
+ import "./chunk-H2TXOCXH.mjs";
39
+ import {
40
+ DEFAULT_CONFIG,
41
+ DISPLAY_IDENTITY_KINDS,
42
+ buildAddAccountUrl,
43
+ buildSignInUrl
44
+ } from "./chunk-VRLFZAGK.mjs";
45
+ import {
46
+ FRAGMENT_TTL_MS,
47
+ keepFragmentOnReturnLinks,
48
+ restoreReturnFragment,
49
+ returnToHere,
50
+ stripFragment,
51
+ withoutFragment
52
+ } from "./chunk-CYR7J4UY.mjs";
53
+ import {
54
+ TAB_ACCOUNT_HINT_KEY,
55
+ TAB_ADOPT_HASH,
56
+ TAB_SESSION_HEADER,
57
+ TAB_SESSION_STORAGE_KEY,
58
+ clearTabSession,
59
+ consumeTabAccountHint,
60
+ consumeTabAdoptMarker,
61
+ installTabFetchInterceptor,
62
+ installTabLinkDecorator,
63
+ readTabSession,
64
+ tabSessionHeader,
65
+ writeTabSession
66
+ } from "./chunk-TJJNTITX.mjs";
67
+ export {
68
+ DEFAULT_CONFIG,
69
+ DISPLAY_IDENTITY_KINDS,
70
+ FRAGMENT_TTL_MS,
71
+ OcAccountChip,
72
+ OcAccountPill,
73
+ OcAddressInput,
74
+ OcBond,
75
+ OcLinkedIdentities,
76
+ OcSessionProvider,
77
+ OcSignIn,
78
+ OcSignInButton,
79
+ SUDO_ACCOUNT_MISMATCH_MESSAGE,
80
+ TAB_ACCOUNT_HINT_KEY,
81
+ TAB_ADOPT_HASH,
82
+ TAB_SESSION_HEADER,
83
+ TAB_SESSION_STORAGE_KEY,
84
+ bondFloorsText,
85
+ buildAddAccountUrl,
86
+ buildSignInUrl,
87
+ clearTabSession,
88
+ consumeTabAccountHint,
89
+ consumeTabAdoptMarker,
90
+ familyReturnTarget,
91
+ fetchOcBond,
92
+ fetchOcLinkedIdentities,
93
+ handleSudoRequired,
94
+ installTabFetchInterceptor,
95
+ installTabLinkDecorator,
96
+ isSudoAccountMismatch,
97
+ keepFragmentOnReturnLinks,
98
+ readTabSession,
99
+ redirectToSudo,
100
+ restoreReturnFragment,
101
+ returnToHere,
102
+ safeReturnTo,
103
+ stripFragment,
104
+ tabSessionHeader,
105
+ useOcAddressSuggestion,
106
+ useOcSession,
107
+ useOptionalOcSession,
108
+ useStepUpAuth,
109
+ useWebAuthnList,
110
+ useWebAuthnRegister,
111
+ withoutFragment,
112
+ writeTabSession
292
113
  };
293
- function resolveConfig(cfg) {
294
- return { ...DEFAULT_CONFIG, ...cfg ?? {} };
295
- }
296
- function buildSignInUrl(cfg, returnTo) {
297
- const base = `${cfg.authOrigin}${cfg.signInPath}`;
298
- if (!returnTo) return base;
299
- const u = new URL(base);
300
- u.searchParams.set("return_to", stripFragment(returnTo));
301
- return u.toString();
302
- }
303
- function buildAddAccountUrl(cfg, returnTo) {
304
- const u = new URL(buildSignInUrl(cfg, returnTo));
305
- u.searchParams.set("add", "1");
306
- return u.toString();
307
- }
308
- var SessionContext = React4.createContext(null);
309
- function normalizeDisplayIdentity(raw, didOc) {
310
- const di = raw.display_identity ?? raw.displayIdentity;
311
- if (di && typeof di === "object" && typeof di.value === "string" && di.value.length > 0 && typeof di.kind === "string" && DISPLAY_IDENTITY_KINDS.includes(di.kind)) {
312
- return { kind: di.kind, value: di.value };
313
- }
314
- return { kind: "did", value: didOc };
315
- }
316
- function normalizeRosterEntry(raw) {
317
- const didOc = raw.did_oc ?? raw.didOc;
318
- if (!didOc) return null;
319
- const di = raw.display_identity ?? raw.displayIdentity;
320
- const displayIdentity = di && typeof di === "object" && typeof di.value === "string" && di.value.length > 0 && typeof di.kind === "string" && DISPLAY_IDENTITY_KINDS.includes(di.kind) ? { kind: di.kind, value: di.value } : { kind: "did", value: didOc };
321
- return {
322
- didOc,
323
- displayName: raw.display_name ?? raw.displayName ?? null,
324
- primaryBtc: raw.primary_btc ?? raw.primaryBtc ?? null,
325
- displayIdentity,
326
- lastSeenAt: raw.last_seen_at ?? raw.lastSeenAt ?? null
327
- };
328
- }
329
- async function fetchHostRoster(cfg) {
330
- try {
331
- const res = await fetch(`${cfg.authOrigin}${cfg.mePath}`, {
332
- method: "GET",
333
- credentials: "include",
334
- headers: { Accept: "application/json", ...tabSessionHeader() }
335
- });
336
- if (!res.ok) return [];
337
- const body = await res.json();
338
- return Array.isArray(body.roster) ? body.roster.map(normalizeRosterEntry).filter((r) => r !== null) : [];
339
- } catch {
340
- return [];
341
- }
342
- }
343
- function normalizeAccount(raw) {
344
- if (!raw) return null;
345
- const didOc = raw.did_oc ?? raw.didOc;
346
- const accountId = raw.id ?? raw.account_id ?? raw.accountId;
347
- if (!didOc || !accountId) return null;
348
- return {
349
- accountId,
350
- didOc,
351
- primaryBtc: raw.primary_btc ?? raw.primaryBtc ?? null,
352
- hasEmail: raw.has_email ?? raw.hasEmail ?? false,
353
- displayName: raw.display_name ?? raw.displayName ?? null,
354
- nostrNpub: raw.nostr_npub ?? raw.nostrNpub ?? null,
355
- homeFederation: raw.home_federation_slug ?? raw.homeFederation ?? null,
356
- signingMethod: raw.signing_method ?? raw.signingMethod ?? null,
357
- isOwner: Boolean(raw.is_owner ?? raw.isOwner ?? false),
358
- displayIdentity: normalizeDisplayIdentity(raw, didOc)
359
- };
360
- }
361
- function OcSessionProvider({
362
- children,
363
- config,
364
- defaultReturnTo
365
- }) {
366
- const cfg = React4.useMemo(() => resolveConfig(config), [config]);
367
- React4.useState(() => {
368
- if (typeof window !== "undefined") {
369
- consumeTabAdoptMarker();
370
- restoreReturnFragment();
371
- }
372
- return null;
373
- });
374
- const [account, setAccount] = React4.useState(null);
375
- const [roster, setRoster] = React4.useState([]);
376
- const [status, setStatus] = React4.useState("loading");
377
- const [error, setError] = React4.useState(null);
378
- const [tabPinned, setTabPinned] = React4.useState(false);
379
- const pinInFlightRef = React4.useRef(false);
380
- const pinThisTab = React4.useCallback(
381
- async (didOc) => {
382
- if (!cfg.multiAccount || pinInFlightRef.current || readTabSession()) return;
383
- pinInFlightRef.current = true;
384
- try {
385
- const res = await fetch(`${cfg.authOrigin}/api/auth/tab`, {
386
- method: "POST",
387
- credentials: "include",
388
- headers: { Accept: "application/json" }
389
- });
390
- if (!res.ok) return;
391
- const body = await res.json();
392
- if (body.ok && typeof body.token === "string" && body.account?.did_oc === didOc) {
393
- writeTabSession({ token: body.token, didOc });
394
- setTabPinned(true);
395
- }
396
- } catch {
397
- } finally {
398
- pinInFlightRef.current = false;
399
- }
400
- },
401
- [cfg.authOrigin, cfg.multiAccount]
402
- );
403
- const refresh = React4.useCallback(async () => {
404
- if (typeof window === "undefined") return;
405
- try {
406
- let pin = readTabSession();
407
- let res = await fetch(cfg.mePath, {
408
- method: "GET",
409
- credentials: "include",
410
- headers: { Accept: "application/json", ...tabSessionHeader() }
411
- });
412
- if (res.status === 401 && pin) {
413
- clearTabSession();
414
- pin = null;
415
- setTabPinned(false);
416
- res = await fetch(cfg.mePath, {
417
- method: "GET",
418
- credentials: "include",
419
- headers: { Accept: "application/json" }
420
- });
421
- }
422
- if (res.status === 401) {
423
- setAccount(null);
424
- setRoster([]);
425
- setStatus("anonymous");
426
- setError(null);
427
- return;
428
- }
429
- if (!res.ok) {
430
- setStatus("error");
431
- setError(new Error(`me endpoint returned ${res.status}`));
432
- return;
433
- }
434
- const body = await res.json();
435
- const acct = normalizeAccount(body.account);
436
- if (acct && pin && acct.didOc !== pin.didOc) {
437
- clearTabSession();
438
- pin = null;
439
- }
440
- setTabPinned(pin !== null);
441
- const rosterEntries = Array.isArray(body.roster) ? body.roster.map(normalizeRosterEntry).filter((r) => r !== null) : [];
442
- setAccount(acct);
443
- setRoster(rosterEntries);
444
- setStatus(acct ? "authenticated" : "anonymous");
445
- setError(null);
446
- if (acct && !pin) void pinThisTab(acct.didOc);
447
- const currentOrigin = typeof window !== "undefined" ? window.location.origin : null;
448
- if (cfg.multiAccount && acct && rosterEntries.length === 0 && currentOrigin !== null && !cfg.authOrigin.startsWith(currentOrigin)) {
449
- const peers = await fetchHostRoster(cfg);
450
- if (peers.length > 0) setRoster(peers);
451
- }
452
- } catch (err) {
453
- setStatus("error");
454
- setError(err instanceof Error ? err : new Error(String(err)));
455
- }
456
- }, [cfg, pinThisTab]);
457
- React4.useEffect(() => {
458
- let cancelled = false;
459
- consumeTabAdoptMarker();
460
- void (async () => {
461
- if (cfg.multiAccount) await consumeTabAccountHint(cfg.authOrigin);
462
- if (!cancelled) void refresh();
463
- })();
464
- return () => {
465
- cancelled = true;
466
- };
467
- }, [refresh, cfg.authOrigin, cfg.multiAccount]);
468
- React4.useEffect(() => keepFragmentOnReturnLinks(), []);
469
- React4.useEffect(
470
- () => cfg.multiAccount ? installTabFetchInterceptor(cfg.authOrigin) : void 0,
471
- [cfg.authOrigin, cfg.multiAccount]
472
- );
473
- React4.useEffect(
474
- () => cfg.multiAccount ? installTabLinkDecorator(cfg.authOrigin) : void 0,
475
- [cfg.authOrigin, cfg.multiAccount]
476
- );
477
- const signOut = React4.useCallback(
478
- async (opts) => {
479
- const scope = opts?.scope ?? "all";
480
- try {
481
- const url = new URL(`${cfg.authOrigin}${cfg.logoutPath}`);
482
- if (scope === "current") url.searchParams.set("scope", "current");
483
- const res = await fetch(url.toString(), {
484
- method: "POST",
485
- credentials: "include",
486
- // Per-tab · carry the pin so scope='current' signs out
487
- // the account THIS tab is operating as, not whichever
488
- // account the shared cookie happens to point at.
489
- headers: { ...tabSessionHeader() },
490
- // `keepalive` lets the logout round-trip complete even if
491
- // the caller hard-navigates away in the same tick (e.g.
492
- // `<OcAccountMenu>` redirects home immediately on sign-out).
493
- // Without it the in-flight request is cancelled on unload
494
- // and the `.ochk.io` cookie may never get cleared.
495
- keepalive: true
496
- });
497
- clearTabSession();
498
- setTabPinned(false);
499
- if (scope === "current" && res.ok) {
500
- await refresh();
501
- return;
502
- }
503
- } catch {
504
- clearTabSession();
505
- setTabPinned(false);
506
- }
507
- setAccount(null);
508
- setRoster([]);
509
- setStatus("anonymous");
510
- },
511
- [cfg.authOrigin, cfg.logoutPath, refresh]
512
- );
513
- const switchAccount = React4.useCallback(
514
- async (didOc) => {
515
- if (typeof window === "undefined") return;
516
- const res = await fetch(`${cfg.authOrigin}/api/auth/switch`, {
517
- method: "POST",
518
- credentials: "include",
519
- headers: { "Content-Type": "application/json", ...tabSessionHeader() },
520
- body: JSON.stringify({ did_oc: didOc })
521
- });
522
- if (!res.ok) {
523
- let reason = `http_${res.status}`;
524
- try {
525
- const body = await res.json();
526
- if (body.reason) reason = body.reason;
527
- } catch {
528
- }
529
- throw new Error(`[@orangecheck/auth-client] switchAccount failed: ${reason}`);
530
- }
531
- try {
532
- const body = await res.json();
533
- if (typeof body.token === "string" && body.account?.did_oc) {
534
- writeTabSession({ token: body.token, didOc: body.account.did_oc });
535
- setTabPinned(true);
536
- }
537
- } catch {
538
- }
539
- await refresh();
540
- },
541
- [cfg.authOrigin, refresh]
542
- );
543
- const addAccountUrl = React4.useCallback(
544
- (returnTo) => {
545
- const rt = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
546
- return buildAddAccountUrl(cfg, rt);
547
- },
548
- [cfg]
549
- );
550
- const setDisplayIdentity = React4.useCallback(
551
- async (kind) => {
552
- if (typeof window === "undefined") return;
553
- const res = await fetch(`${cfg.authOrigin}/api/auth/account`, {
554
- method: "PATCH",
555
- credentials: "include",
556
- headers: { "Content-Type": "application/json", ...tabSessionHeader() },
557
- body: JSON.stringify({ display_identity: kind })
558
- });
559
- if (!res.ok) {
560
- let reason = `http_${res.status}`;
561
- try {
562
- const body = await res.json();
563
- if (body.reason) reason = body.reason;
564
- } catch {
565
- }
566
- throw new Error(`[@orangecheck/auth-client] setDisplayIdentity failed: ${reason}`);
567
- }
568
- try {
569
- const body = await res.json();
570
- const pin = readTabSession();
571
- if (pin && typeof body.token === "string" && body.account?.did_oc === pin.didOc) {
572
- writeTabSession({ token: body.token, didOc: pin.didOc });
573
- }
574
- } catch {
575
- }
576
- await refresh();
577
- },
578
- [cfg.authOrigin, refresh]
579
- );
580
- const value = React4.useMemo(() => {
581
- const returnTo = defaultReturnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
582
- return {
583
- status,
584
- account,
585
- roster,
586
- tabPinned,
587
- error,
588
- refresh,
589
- signOut,
590
- switchAccount,
591
- addAccountUrl,
592
- setDisplayIdentity,
593
- signInUrl: buildSignInUrl(cfg, returnTo)
594
- };
595
- }, [
596
- status,
597
- account,
598
- roster,
599
- tabPinned,
600
- error,
601
- refresh,
602
- signOut,
603
- switchAccount,
604
- addAccountUrl,
605
- setDisplayIdentity,
606
- cfg,
607
- defaultReturnTo
608
- ]);
609
- return /* @__PURE__ */ jsx(SessionContext.Provider, { value, children });
610
- }
611
- function useOcSession() {
612
- const ctx = React4.useContext(SessionContext);
613
- if (!ctx) {
614
- throw new Error(
615
- "[@orangecheck/auth-client] useOcSession() must be called inside <OcSessionProvider>"
616
- );
617
- }
618
- return ctx;
619
- }
620
- function useOptionalOcSession() {
621
- return React4.useContext(SessionContext);
622
- }
623
- function shortenAddress(addr) {
624
- if (addr.length <= 12) return addr;
625
- return `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`;
626
- }
627
- function shortenAddressMid(addr) {
628
- if (addr.length <= 16) return addr;
629
- return `${addr.slice(0, 8)}\u2026${addr.slice(-6)}`;
630
- }
631
- function isPrefixOf(value, target) {
632
- return target.toLowerCase().startsWith(value.toLowerCase());
633
- }
634
- var listboxIdCounter = 0;
635
- function useUniqueId(prefix) {
636
- const [id] = React4.useState(() => `${prefix}-${++listboxIdCounter}`);
637
- return id;
638
- }
639
- function OcAccountChip({
640
- dashboardUrl = "https://ochk.io/dashboard",
641
- signInUrl: signInUrlOverride,
642
- signInLabel = "sign in",
643
- className,
644
- triggerClassName,
645
- popoverClassName,
646
- menuItemClassName
647
- }) {
648
- const { status, account, signInUrl: sessionSignInUrl, signOut } = useOcSession();
649
- const signInUrl = signInUrlOverride ?? sessionSignInUrl;
650
- const [open, setOpen] = React4.useState(false);
651
- const wrapRef = React4.useRef(null);
652
- React4.useEffect(() => {
653
- if (!open) return;
654
- function handleClickOutside(e) {
655
- if (wrapRef.current && !wrapRef.current.contains(e.target)) {
656
- setOpen(false);
657
- }
658
- }
659
- function handleEscape(e) {
660
- if (e.key === "Escape") setOpen(false);
661
- }
662
- document.addEventListener("mousedown", handleClickOutside);
663
- document.addEventListener("keydown", handleEscape);
664
- return () => {
665
- document.removeEventListener("mousedown", handleClickOutside);
666
- document.removeEventListener("keydown", handleEscape);
667
- };
668
- }, [open]);
669
- if (status === "loading") return null;
670
- if (status !== "authenticated" || !account) {
671
- return /* @__PURE__ */ jsx(
672
- "a",
673
- {
674
- href: signInUrl,
675
- className: triggerClassName ?? className,
676
- "data-oc-account-chip-signin": "",
677
- children: signInLabel
678
- }
679
- );
680
- }
681
- return /* @__PURE__ */ jsxs(
682
- "div",
683
- {
684
- ref: wrapRef,
685
- className,
686
- "data-oc-account-chip": "",
687
- style: { position: "relative", display: "inline-block" },
688
- children: [
689
- /* @__PURE__ */ jsxs(
690
- "button",
691
- {
692
- type: "button",
693
- onClick: () => setOpen((v) => !v),
694
- "aria-haspopup": "menu",
695
- "aria-expanded": open,
696
- "aria-label": `Signed in as ${account.didOc}. Open account menu.`,
697
- className: triggerClassName,
698
- "data-oc-account-chip-trigger": "",
699
- style: triggerClassName ? void 0 : {
700
- display: "inline-flex",
701
- alignItems: "center",
702
- gap: "0.4rem",
703
- padding: "0.25rem 0.625rem",
704
- borderRadius: "9999px",
705
- border: "1px solid var(--border, #27272a)",
706
- background: "color-mix(in oklch, var(--muted, #27272a) 40%, transparent)",
707
- color: "var(--foreground, #fafafa)",
708
- fontFamily: "ui-monospace, SFMono-Regular, monospace",
709
- fontSize: 12,
710
- cursor: "pointer"
711
- },
712
- children: [
713
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: "#22c55e", fontSize: 8 }, children: "\u25CF" }),
714
- /* @__PURE__ */ jsx("span", { children: account.displayName ?? shortenAddress(account.didOc) }),
715
- /* @__PURE__ */ jsx(
716
- "svg",
717
- {
718
- width: "9",
719
- height: "9",
720
- viewBox: "0 0 10 10",
721
- "aria-hidden": "true",
722
- style: {
723
- transition: "transform 120ms",
724
- transform: open ? "rotate(180deg)" : "rotate(0)",
725
- opacity: 0.6
726
- },
727
- children: /* @__PURE__ */ jsx(
728
- "path",
729
- {
730
- d: "M2 4 L5 7 L8 4",
731
- stroke: "currentColor",
732
- strokeWidth: "1.5",
733
- fill: "none",
734
- strokeLinecap: "round"
735
- }
736
- )
737
- }
738
- )
739
- ]
740
- }
741
- ),
742
- open && /* @__PURE__ */ jsxs(
743
- "div",
744
- {
745
- role: "menu",
746
- "aria-label": "Account menu",
747
- className: popoverClassName,
748
- "data-oc-account-chip-popover": "",
749
- style: popoverClassName ? { position: "absolute", zIndex: 50, top: "calc(100% + 6px)", right: 0 } : {
750
- position: "absolute",
751
- zIndex: 50,
752
- top: "calc(100% + 6px)",
753
- right: 0,
754
- minWidth: 240,
755
- background: "var(--popover, #0a0a0a)",
756
- color: "var(--popover-foreground, #fafafa)",
757
- border: "1px solid var(--border, #27272a)",
758
- boxShadow: "0 10px 32px -4px rgba(0,0,0,0.35)",
759
- fontFamily: "ui-monospace, SFMono-Regular, monospace"
760
- },
761
- children: [
762
- /* @__PURE__ */ jsxs(
763
- "div",
764
- {
765
- "data-oc-account-chip-header": "",
766
- style: {
767
- padding: "0.75rem 0.75rem 0.625rem",
768
- borderBottom: "1px solid var(--border, #27272a)"
769
- },
770
- children: [
771
- /* @__PURE__ */ jsx(
772
- "div",
773
- {
774
- "data-oc-account-chip-label": "",
775
- style: {
776
- color: "var(--primary, #f97316)",
777
- fontSize: 10,
778
- letterSpacing: "0.18em",
779
- textTransform: "uppercase",
780
- marginBottom: 4
781
- },
782
- children: "\xA7 signed in"
783
- }
784
- ),
785
- /* @__PURE__ */ jsx(
786
- "div",
787
- {
788
- "data-oc-account-chip-address": "",
789
- style: {
790
- color: "var(--popover-foreground, #fafafa)",
791
- fontSize: 11,
792
- wordBreak: "break-all",
793
- lineHeight: 1.35
794
- },
795
- children: account.didOc
796
- }
797
- ),
798
- account.displayName ? /* @__PURE__ */ jsx(
799
- "div",
800
- {
801
- style: {
802
- color: "var(--muted-foreground, #a1a1aa)",
803
- fontSize: 10,
804
- marginTop: 4,
805
- letterSpacing: "0.06em"
806
- },
807
- children: account.displayName
808
- }
809
- ) : null
810
- ]
811
- }
812
- ),
813
- /* @__PURE__ */ jsxs("div", { role: "none", style: { padding: "0.25rem" }, children: [
814
- dashboardUrl ? /* @__PURE__ */ jsxs(
815
- "a",
816
- {
817
- role: "menuitem",
818
- href: dashboardUrl,
819
- onClick: () => setOpen(false),
820
- className: menuItemClassName,
821
- "data-oc-account-chip-item": "",
822
- style: menuItemClassName ? void 0 : {
823
- display: "flex",
824
- alignItems: "center",
825
- gap: "0.5rem",
826
- padding: "0.5rem 0.625rem",
827
- fontSize: 12,
828
- color: "var(--popover-foreground, #fafafa)",
829
- textDecoration: "none",
830
- cursor: "pointer"
831
- },
832
- children: [
833
- /* @__PURE__ */ jsx(
834
- "span",
835
- {
836
- "aria-hidden": "true",
837
- style: { color: "var(--muted-foreground, #a1a1aa)" },
838
- children: "\u2192"
839
- }
840
- ),
841
- /* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: "dashboard" }),
842
- /* @__PURE__ */ jsx(
843
- "span",
844
- {
845
- style: {
846
- color: "var(--muted-foreground, #a1a1aa)",
847
- fontSize: 10
848
- },
849
- children: "\u2197"
850
- }
851
- )
852
- ]
853
- }
854
- ) : null,
855
- /* @__PURE__ */ jsxs(
856
- "button",
857
- {
858
- type: "button",
859
- role: "menuitem",
860
- onClick: () => {
861
- setOpen(false);
862
- void signOut();
863
- },
864
- className: menuItemClassName,
865
- "data-oc-account-chip-item": "",
866
- "data-oc-account-chip-signout": "",
867
- style: menuItemClassName ? void 0 : {
868
- display: "flex",
869
- alignItems: "center",
870
- gap: "0.5rem",
871
- width: "100%",
872
- padding: "0.5rem 0.625rem",
873
- fontSize: 12,
874
- color: "var(--popover-foreground, #fafafa)",
875
- background: "transparent",
876
- border: 0,
877
- fontFamily: "inherit",
878
- textAlign: "left",
879
- cursor: "pointer"
880
- },
881
- children: [
882
- /* @__PURE__ */ jsx(
883
- "span",
884
- {
885
- "aria-hidden": "true",
886
- style: { color: "var(--muted-foreground, #a1a1aa)" },
887
- children: "\u2192"
888
- }
889
- ),
890
- /* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: "sign out" })
891
- ]
892
- }
893
- )
894
- ] })
895
- ]
896
- }
897
- )
898
- ]
899
- }
900
- );
901
- }
902
- function OcSignInButton({
903
- label = "sign in",
904
- eager = false,
905
- className,
906
- signInUrl: signInUrlOverride,
907
- ...rest
908
- }) {
909
- const { status, signInUrl: sessionSignInUrl } = useOcSession();
910
- const href = signInUrlOverride ?? sessionSignInUrl;
911
- if (status === "authenticated") return null;
912
- if (!eager && status === "loading") return null;
913
- return /* @__PURE__ */ jsx(
914
- "a",
915
- {
916
- ...rest,
917
- href,
918
- className,
919
- "data-oc-sign-in-button": "",
920
- children: label
921
- }
922
- );
923
- }
924
- function OcAccountPill({
925
- dashboardUrl,
926
- render,
927
- className,
928
- ...rest
929
- }) {
930
- const { status, account, signOut } = useOcSession();
931
- if (status !== "authenticated" || !account) return null;
932
- const label = render ? render({ address: account.didOc, displayName: account.displayName }) : account.displayName ?? shortenAddress(account.didOc);
933
- return /* @__PURE__ */ jsxs(
934
- "div",
935
- {
936
- ...rest,
937
- className,
938
- "data-oc-account-pill": "",
939
- style: { display: "inline-flex", alignItems: "center", gap: "0.5rem", ...rest.style ?? {} },
940
- children: [
941
- dashboardUrl ? /* @__PURE__ */ jsx("a", { href: dashboardUrl, children: label }) : /* @__PURE__ */ jsx("span", { children: label }),
942
- /* @__PURE__ */ jsx(
943
- "button",
944
- {
945
- type: "button",
946
- onClick: () => {
947
- void signOut();
948
- },
949
- "aria-label": "Sign out",
950
- style: {
951
- background: "none",
952
- border: "none",
953
- cursor: "pointer",
954
- color: "inherit",
955
- font: "inherit",
956
- padding: 0
957
- },
958
- children: "sign out"
959
- }
960
- )
961
- ]
962
- }
963
- );
964
- }
965
- function accountAddress(account) {
966
- if (account?.primaryBtc) return account.primaryBtc;
967
- return account?.displayIdentity?.kind === "btc" ? account.displayIdentity.value : null;
968
- }
969
- function useOcAddressSuggestion(options) {
970
- const {
971
- value,
972
- onValueChange,
973
- suggestionLabel = "use your address",
974
- popoverClassName,
975
- suggestionClassName
976
- } = options;
977
- const { status, account } = useOcSession();
978
- const sessionAddress = status === "authenticated" ? accountAddress(account) : null;
979
- const [open, setOpen] = React4.useState(false);
980
- const [highlighted, setHighlighted] = React4.useState(false);
981
- const blurTimer = React4.useRef(null);
982
- const listboxId = useUniqueId("oc-addr-listbox");
983
- const optionId = `${listboxId}-opt`;
984
- const valueMatchesSession = sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();
985
- const canSuggest = sessionAddress != null && sessionAddress.length > 0 && !valueMatchesSession && isPrefixOf(value, sessionAddress);
986
- const showPopover = open && canSuggest;
987
- function selectSuggestion() {
988
- if (!sessionAddress) return;
989
- onValueChange(sessionAddress);
990
- setOpen(false);
991
- setHighlighted(false);
992
- }
993
- React4.useEffect(() => {
994
- return () => {
995
- if (blurTimer.current != null) window.clearTimeout(blurTimer.current);
996
- };
997
- }, []);
998
- const inputProps = {
999
- onFocus: () => {
1000
- if (blurTimer.current != null) {
1001
- window.clearTimeout(blurTimer.current);
1002
- blurTimer.current = null;
1003
- }
1004
- setOpen(true);
1005
- },
1006
- onBlur: () => {
1007
- blurTimer.current = window.setTimeout(() => {
1008
- setOpen(false);
1009
- setHighlighted(false);
1010
- }, 120);
1011
- },
1012
- onKeyDown: (e) => {
1013
- if (!showPopover) return;
1014
- if (e.key === "ArrowDown") {
1015
- e.preventDefault();
1016
- setHighlighted(true);
1017
- } else if (e.key === "ArrowUp") {
1018
- e.preventDefault();
1019
- setHighlighted(false);
1020
- } else if (e.key === "Escape") {
1021
- e.preventDefault();
1022
- setOpen(false);
1023
- setHighlighted(false);
1024
- } else if (e.key === "Enter" && highlighted) {
1025
- e.preventDefault();
1026
- selectSuggestion();
1027
- }
1028
- },
1029
- role: "combobox",
1030
- "aria-haspopup": "listbox",
1031
- "aria-expanded": showPopover,
1032
- "aria-controls": showPopover ? listboxId : void 0,
1033
- "aria-activedescendant": highlighted ? optionId : void 0,
1034
- autoComplete: "off",
1035
- spellCheck: false
1036
- };
1037
- const popover = showPopover && sessionAddress ? /* @__PURE__ */ jsx(
1038
- "div",
1039
- {
1040
- id: listboxId,
1041
- role: "listbox",
1042
- "data-oc-address-popover": "",
1043
- className: popoverClassName,
1044
- style: {
1045
- position: "absolute",
1046
- zIndex: 50,
1047
- top: "calc(100% + 4px)",
1048
- left: 0,
1049
- right: 0
1050
- },
1051
- onMouseDown: (e) => e.preventDefault(),
1052
- children: /* @__PURE__ */ jsxs(
1053
- "button",
1054
- {
1055
- type: "button",
1056
- id: optionId,
1057
- role: "option",
1058
- "aria-selected": highlighted,
1059
- "data-oc-address-suggestion": "",
1060
- "data-highlighted": highlighted ? "" : void 0,
1061
- className: suggestionClassName,
1062
- onClick: selectSuggestion,
1063
- onMouseEnter: () => setHighlighted(true),
1064
- onMouseLeave: () => setHighlighted(false),
1065
- style: {
1066
- display: "flex",
1067
- alignItems: "center",
1068
- justifyContent: "space-between",
1069
- gap: "0.75rem",
1070
- width: "100%",
1071
- textAlign: "left",
1072
- cursor: "pointer",
1073
- font: "inherit",
1074
- background: "inherit",
1075
- color: "inherit",
1076
- border: 0,
1077
- padding: "inherit"
1078
- },
1079
- children: [
1080
- /* @__PURE__ */ jsx("span", { "data-oc-address-suggestion-label": "", children: suggestionLabel }),
1081
- /* @__PURE__ */ jsx(
1082
- "span",
1083
- {
1084
- "data-oc-address-suggestion-value": "",
1085
- style: { fontFamily: "ui-monospace, monospace" },
1086
- children: shortenAddressMid(sessionAddress)
1087
- }
1088
- )
1089
- ]
1090
- }
1091
- )
1092
- }
1093
- ) : null;
1094
- return { inputProps, popover };
1095
- }
1096
- var OcAddressInput = React4.forwardRef(
1097
- function OcAddressInput2({
1098
- value,
1099
- onValueChange,
1100
- suggestionLabel = "use your address",
1101
- wrapperClassName,
1102
- popoverClassName,
1103
- suggestionClassName,
1104
- onFocus,
1105
- onBlur,
1106
- onKeyDown,
1107
- ...rest
1108
- }, forwardedRef) {
1109
- const { status, account } = useOcSession();
1110
- const sessionAddress = status === "authenticated" ? accountAddress(account) : null;
1111
- const [open, setOpen] = React4.useState(false);
1112
- const [highlighted, setHighlighted] = React4.useState(false);
1113
- const innerRef = React4.useRef(null);
1114
- const setRef = (node) => {
1115
- innerRef.current = node;
1116
- if (typeof forwardedRef === "function") forwardedRef(node);
1117
- else if (forwardedRef) forwardedRef.current = node;
1118
- };
1119
- const blurTimer = React4.useRef(null);
1120
- const listboxId = useUniqueId("oc-addr-listbox");
1121
- const optionId = `${listboxId}-opt`;
1122
- const valueMatchesSession = sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();
1123
- const canSuggest = sessionAddress != null && sessionAddress.length > 0 && !valueMatchesSession && isPrefixOf(value, sessionAddress);
1124
- const showPopover = open && canSuggest;
1125
- function selectSuggestion() {
1126
- if (!sessionAddress) return;
1127
- onValueChange(sessionAddress);
1128
- setOpen(false);
1129
- setHighlighted(false);
1130
- innerRef.current?.focus();
1131
- }
1132
- function handleFocus(e) {
1133
- if (blurTimer.current != null) {
1134
- window.clearTimeout(blurTimer.current);
1135
- blurTimer.current = null;
1136
- }
1137
- setOpen(true);
1138
- onFocus?.(e);
1139
- }
1140
- function handleBlur(e) {
1141
- blurTimer.current = window.setTimeout(() => {
1142
- setOpen(false);
1143
- setHighlighted(false);
1144
- }, 120);
1145
- onBlur?.(e);
1146
- }
1147
- function handleKeyDown(e) {
1148
- if (showPopover) {
1149
- if (e.key === "ArrowDown") {
1150
- e.preventDefault();
1151
- setHighlighted(true);
1152
- } else if (e.key === "ArrowUp") {
1153
- e.preventDefault();
1154
- setHighlighted(false);
1155
- } else if (e.key === "Escape") {
1156
- e.preventDefault();
1157
- setOpen(false);
1158
- setHighlighted(false);
1159
- } else if (e.key === "Enter" && highlighted) {
1160
- e.preventDefault();
1161
- selectSuggestion();
1162
- }
1163
- }
1164
- onKeyDown?.(e);
1165
- }
1166
- React4.useEffect(() => {
1167
- return () => {
1168
- if (blurTimer.current != null) window.clearTimeout(blurTimer.current);
1169
- };
1170
- }, []);
1171
- return /* @__PURE__ */ jsxs(
1172
- "div",
1173
- {
1174
- "data-oc-address-input": "",
1175
- className: wrapperClassName,
1176
- style: { position: "relative" },
1177
- children: [
1178
- /* @__PURE__ */ jsx(
1179
- "input",
1180
- {
1181
- ...rest,
1182
- ref: setRef,
1183
- value,
1184
- onChange: (e) => onValueChange(e.target.value),
1185
- onFocus: handleFocus,
1186
- onBlur: handleBlur,
1187
- onKeyDown: handleKeyDown,
1188
- role: "combobox",
1189
- "aria-haspopup": "listbox",
1190
- "aria-expanded": showPopover,
1191
- "aria-controls": showPopover ? listboxId : void 0,
1192
- "aria-activedescendant": highlighted ? optionId : void 0,
1193
- autoComplete: "off",
1194
- spellCheck: false
1195
- }
1196
- ),
1197
- showPopover && sessionAddress && /* @__PURE__ */ jsx(
1198
- "div",
1199
- {
1200
- id: listboxId,
1201
- role: "listbox",
1202
- "data-oc-address-popover": "",
1203
- className: popoverClassName,
1204
- style: {
1205
- position: "absolute",
1206
- zIndex: 50,
1207
- top: "calc(100% + 4px)",
1208
- left: 0,
1209
- right: 0
1210
- },
1211
- onMouseDown: (e) => e.preventDefault(),
1212
- children: /* @__PURE__ */ jsxs(
1213
- "button",
1214
- {
1215
- type: "button",
1216
- id: optionId,
1217
- role: "option",
1218
- "aria-selected": highlighted,
1219
- "data-oc-address-suggestion": "",
1220
- "data-highlighted": highlighted ? "" : void 0,
1221
- className: suggestionClassName,
1222
- onClick: selectSuggestion,
1223
- onMouseEnter: () => setHighlighted(true),
1224
- onMouseLeave: () => setHighlighted(false),
1225
- style: {
1226
- display: "flex",
1227
- alignItems: "center",
1228
- justifyContent: "space-between",
1229
- gap: "0.75rem",
1230
- width: "100%",
1231
- textAlign: "left",
1232
- cursor: "pointer",
1233
- font: "inherit",
1234
- background: "inherit",
1235
- color: "inherit",
1236
- border: "inherit",
1237
- padding: "inherit"
1238
- },
1239
- children: [
1240
- /* @__PURE__ */ jsx("span", { "data-oc-address-suggestion-label": "", children: suggestionLabel }),
1241
- /* @__PURE__ */ jsx(
1242
- "span",
1243
- {
1244
- "data-oc-address-suggestion-value": "",
1245
- style: { fontFamily: "ui-monospace, monospace" },
1246
- children: shortenAddressMid(sessionAddress)
1247
- }
1248
- )
1249
- ]
1250
- }
1251
- )
1252
- }
1253
- )
1254
- ]
1255
- }
1256
- );
1257
- }
1258
- );
1259
-
1260
- // src/sudo.tsx
1261
- function redirectToSudo(args = {}) {
1262
- if (typeof window === "undefined") {
1263
- throw new Error(
1264
- "[@orangecheck/auth-client] redirectToSudo() requires a browser environment"
1265
- );
1266
- }
1267
- const cfg = resolveConfig(args.config);
1268
- const url = new URL("/sudo", cfg.authOrigin);
1269
- const returnTo = withoutFragment(args.returnTo ?? window.location.href);
1270
- if (returnTo) url.searchParams.set("return_to", returnTo);
1271
- if (args.purpose) url.searchParams.set("purpose", args.purpose);
1272
- window.location.assign(url.toString());
1273
- }
1274
- function handleSudoRequired(body, args = {}) {
1275
- if (body && body.reason === "sudo_required") {
1276
- redirectToSudo(args);
1277
- return true;
1278
- }
1279
- return false;
1280
- }
1281
- var SUDO_ACCOUNT_MISMATCH_MESSAGE = "Switch this tab to that account first \u2014 the confirmation on file is for a different one.";
1282
- function isSudoAccountMismatch(body) {
1283
- return body?.reason === "sudo_account_mismatch";
1284
- }
1285
-
1286
- // src/appearance.ts
1287
- var SIGNIN_FONT = "var(--oc-signin-font, ui-monospace, SFMono-Regular, monospace)";
1288
- var SIGNIN_CASE = "var(--oc-signin-case, uppercase)";
1289
- var signinTracking = (em) => `var(--oc-signin-tracking, ${em})`;
1290
- var signinSize = (px) => `calc(${px}px * var(--oc-signin-scale, 1))`;
1291
- var signinRadius = (px) => `var(--oc-signin-radius, ${px}px)`;
1292
- var SIGNIN_BUTTON_RADIUS = "var(--oc-signin-button-radius, 6px)";
1293
- var SIGNIN_STRONG = "var(--oc-signin-strong, 600)";
1294
- var SIGNIN_ARROW = "var(--oc-signin-arrow, inline)";
1295
- var signinRoleSize = (role, fallback) => `var(--oc-signin-${role}-size, ${fallback})`;
1296
- var DEFAULT_AUTH_ORIGIN = "https://ochk.io";
1297
- async function fetchOcLinkedIdentities(opts) {
1298
- const authOrigin = opts?.authOrigin ?? DEFAULT_AUTH_ORIGIN;
1299
- const r = await fetch(`${authOrigin}/api/auth/identities`, {
1300
- credentials: "include",
1301
- headers: { ...tabSessionHeader() }
1302
- });
1303
- if (r.status === 401) return [];
1304
- if (!r.ok) throw new Error(`identities fetch failed: http_${r.status}`);
1305
- const body = await r.json();
1306
- return body.linked ?? [];
1307
- }
1308
- function OcLinkedIdentities({
1309
- authOrigin = DEFAULT_AUTH_ORIGIN,
1310
- className,
1311
- onChange,
1312
- attestationLink = true
1313
- }) {
1314
- const [resp, setResp] = React4.useState(null);
1315
- const [error, setError] = React4.useState(null);
1316
- const [showEmailLink, setShowEmailLink] = React4.useState(false);
1317
- const [showBtcLink, setShowBtcLink] = React4.useState(false);
1318
- const refresh = React4.useCallback(async () => {
1319
- setError(null);
1320
- try {
1321
- const r = await fetch(`${authOrigin}/api/auth/identities`, {
1322
- credentials: "include",
1323
- headers: { ...tabSessionHeader() }
1324
- });
1325
- if (!r.ok) {
1326
- if (r.status === 401) {
1327
- setError("not signed in");
1328
- return;
1329
- }
1330
- throw new Error(`http_${r.status}`);
1331
- }
1332
- setResp(await r.json());
1333
- } catch (e) {
1334
- setError(e instanceof Error ? e.message : "load failed");
1335
- }
1336
- }, [authOrigin]);
1337
- React4.useEffect(() => {
1338
- void refresh();
1339
- }, [refresh]);
1340
- const afterChange = React4.useCallback(() => {
1341
- setShowEmailLink(false);
1342
- setShowBtcLink(false);
1343
- void refresh();
1344
- onChange?.();
1345
- }, [refresh, onChange]);
1346
- if (error) {
1347
- return /* @__PURE__ */ jsxs("div", { className, "data-oc-li": "", style: panelStyle("warn"), role: "alert", children: [
1348
- /* @__PURE__ */ jsx(SectionLabel, { tone: "warn", children: "\xA7 identities \xB7 load deferred" }),
1349
- /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1350
- "Couldn't reach the auth host (",
1351
- error,
1352
- "). Linking is temporarily unavailable; your signed-in identity still works as normal."
1353
- ] }),
1354
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void refresh(), style: ghostBtnStyle(false), children: "retry" })
1355
- ] });
1356
- }
1357
- if (!resp) {
1358
- return /* @__PURE__ */ jsx("div", { className, "data-oc-li": "", style: panelStyle(), children: /* @__PURE__ */ jsx("span", { style: hintStyle, children: "loading\u2026" }) });
1359
- }
1360
- const linked = resp.linked ?? [];
1361
- const emailRows = linked.filter((i) => i.kind === "email");
1362
- const btcRows = linked.filter((i) => i.kind === "btc");
1363
- const hasEmail = emailRows.some((i) => i.value);
1364
- const hasBtc = btcRows.some((i) => i.value);
1365
- const mergedFrom = resp.merged_from ?? [];
1366
- return /* @__PURE__ */ jsxs("div", { className, "data-oc-li": "", style: { display: "grid", gap: 16 }, children: [
1367
- /* @__PURE__ */ jsxs("div", { style: panelStyle(), children: [
1368
- /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 what this is" }),
1369
- /* @__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." })
1370
- ] }),
1371
- mergedFrom.length > 0 && /* @__PURE__ */ jsx(ConsolidationPanel, { entries: mergedFrom }),
1372
- /* @__PURE__ */ jsxs("div", { style: panelStyle("plain"), "data-oc-li-rows": "", children: [
1373
- /* @__PURE__ */ jsx(
1374
- IdentityGroup,
1375
- {
1376
- kind: "email",
1377
- title: "email",
1378
- rows: emailRows,
1379
- authOrigin,
1380
- onChange: afterChange,
1381
- addCta: hasEmail ? null : "link an email",
1382
- onAdd: () => {
1383
- setShowEmailLink((v) => !v);
1384
- setShowBtcLink(false);
1385
- }
1386
- }
1387
- ),
1388
- /* @__PURE__ */ jsx("div", { style: { borderTop: `1px solid ${V.border}` } }),
1389
- /* @__PURE__ */ jsx(
1390
- IdentityGroup,
1391
- {
1392
- kind: "btc",
1393
- title: "bitcoin address",
1394
- rows: btcRows,
1395
- authOrigin,
1396
- onChange: afterChange,
1397
- addCta: hasBtc ? null : "link a Bitcoin address",
1398
- onAdd: () => {
1399
- setShowBtcLink((v) => !v);
1400
- setShowEmailLink(false);
1401
- }
1402
- }
1403
- )
1404
- ] }),
1405
- showEmailLink && /* @__PURE__ */ jsx(
1406
- EmailLinkForm,
1407
- {
1408
- authOrigin,
1409
- onDone: afterChange,
1410
- onCancel: () => setShowEmailLink(false)
1411
- }
1412
- ),
1413
- showBtcLink && /* @__PURE__ */ jsx(
1414
- BtcLinkForm,
1415
- {
1416
- authOrigin,
1417
- didOc: resp.did_oc ?? null,
1418
- onDone: afterChange,
1419
- onCancel: () => setShowBtcLink(false)
1420
- }
1421
- ),
1422
- hasBtc && attestationLink && /* @__PURE__ */ jsx(AttestationLink, {})
1423
- ] });
1424
- }
1425
- function IdentityGroup({
1426
- kind,
1427
- title,
1428
- rows,
1429
- authOrigin,
1430
- onChange,
1431
- addCta,
1432
- onAdd
1433
- }) {
1434
- if (rows.length === 0) {
1435
- return /* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
1436
- /* @__PURE__ */ jsxs("div", { children: [
1437
- /* @__PURE__ */ jsxs("div", { style: rowLabelStyle, children: [
1438
- "\xA7 ",
1439
- title
1440
- ] }),
1441
- /* @__PURE__ */ jsx("div", { style: { ...bodyStyle, marginTop: 4 }, children: "none linked" })
1442
- ] }),
1443
- addCta && /* @__PURE__ */ jsxs("button", { type: "button", onClick: onAdd, style: primaryChipStyle, children: [
1444
- "+ ",
1445
- addCta
1446
- ] })
1447
- ] });
1448
- }
1449
- return /* @__PURE__ */ jsx(Fragment, { children: rows.map((r) => /* @__PURE__ */ jsx(
1450
- IdentityRow,
1451
- {
1452
- row: r,
1453
- kind,
1454
- authOrigin,
1455
- onChange
1456
- },
1457
- r.id
1458
- )) });
1459
- }
1460
- function IdentityRow({
1461
- row,
1462
- kind,
1463
- authOrigin,
1464
- onChange
1465
- }) {
1466
- const [shown, setShown] = React4.useState(false);
1467
- const [confirming, setConfirming] = React4.useState(false);
1468
- const [busy, setBusy] = React4.useState(false);
1469
- const [err, setErr] = React4.useState(null);
1470
- const masked = row.value ? mask(row.value, kind) : "\u2014 pending recovery \u2014";
1471
- const display = shown && row.value ? row.value : masked;
1472
- async function unlink() {
1473
- if (row.is_primary) return;
1474
- setBusy(true);
1475
- setErr(null);
1476
- try {
1477
- const r = await fetch(
1478
- `${authOrigin}/api/auth/identities?id=${encodeURIComponent(row.id)}`,
1479
- { method: "DELETE", credentials: "include" }
1480
- );
1481
- if (!r.ok) {
1482
- const j = await r.json().catch(() => ({}));
1483
- throw new Error(j.reason ?? `http_${r.status}`);
1484
- }
1485
- onChange();
1486
- } catch (e) {
1487
- setErr(e instanceof Error ? e.message : "unlink failed");
1488
- setBusy(false);
1489
- setConfirming(false);
1490
- }
1491
- }
1492
- return /* @__PURE__ */ jsxs("div", { style: rowStyle, "data-oc-li-row": kind, children: [
1493
- /* @__PURE__ */ jsxs("div", { style: { minWidth: 0, flex: 1 }, children: [
1494
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: "4px 12px" }, children: [
1495
- /* @__PURE__ */ jsxs("span", { style: rowLabelStyle, children: [
1496
- "\xA7 ",
1497
- kind === "email" ? "email" : "bitcoin address"
1498
- ] }),
1499
- row.is_primary && /* @__PURE__ */ jsx("span", { style: { ...rowLabelStyle, color: V.primary }, children: "primary \xB7 signup-time" })
1500
- ] }),
1501
- /* @__PURE__ */ jsx(
1502
- "div",
1503
- {
1504
- style: {
1505
- marginTop: 4,
1506
- color: V.foreground,
1507
- fontFamily: SIGNIN_FONT,
1508
- fontSize: signinSize(12),
1509
- wordBreak: "break-all"
1510
- },
1511
- children: display
1512
- }
1513
- ),
1514
- row.verified_at && /* @__PURE__ */ jsxs("div", { style: { ...hintStyle, marginTop: 4 }, children: [
1515
- "verified \xB7 ",
1516
- new Date(row.verified_at).toUTCString()
1517
- ] }),
1518
- err && /* @__PURE__ */ jsx(ErrorLine, { children: err })
1519
- ] }),
1520
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexShrink: 0, gap: 8, flexWrap: "wrap" }, children: [
1521
- row.value && /* @__PURE__ */ jsx(
1522
- "button",
1523
- {
1524
- type: "button",
1525
- onClick: () => setShown((s) => !s),
1526
- style: ghostBtnStyle(false),
1527
- children: shown ? "hide" : "show"
1528
- }
1529
- ),
1530
- !row.is_primary && !confirming && /* @__PURE__ */ jsx(
1531
- "button",
1532
- {
1533
- type: "button",
1534
- onClick: () => setConfirming(true),
1535
- style: dangerBtnStyle(false),
1536
- children: "unlink"
1537
- }
1538
- ),
1539
- !row.is_primary && confirming && /* @__PURE__ */ jsxs(Fragment, { children: [
1540
- /* @__PURE__ */ jsx(
1541
- "button",
1542
- {
1543
- type: "button",
1544
- onClick: () => void unlink(),
1545
- disabled: busy,
1546
- style: dangerBtnStyle(busy),
1547
- children: busy ? "unlinking\u2026" : "confirm unlink"
1548
- }
1549
- ),
1550
- /* @__PURE__ */ jsx(
1551
- "button",
1552
- {
1553
- type: "button",
1554
- onClick: () => setConfirming(false),
1555
- disabled: busy,
1556
- style: ghostBtnStyle(busy),
1557
- children: "cancel"
1558
- }
1559
- )
1560
- ] })
1561
- ] })
1562
- ] });
1563
- }
1564
- function ConsolidationPanel({ entries }) {
1565
- return /* @__PURE__ */ jsxs("div", { style: panelStyle("success"), "data-oc-li-consolidated": "", children: [
1566
- /* @__PURE__ */ jsxs(SectionLabel, { tone: "success", children: [
1567
- "\xA7 consolidated accounts \xB7 ",
1568
- entries.length
1569
- ] }),
1570
- /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1571
- entries.length === 1 ? "A previous OC account" : "Previous OC accounts",
1572
- " you signed up with, folded into this one under dual proof of control. Historical activity is unioned into this account automatically;",
1573
- " ",
1574
- entries.length === 1 ? "it" : "they",
1575
- " can no longer sign in."
1576
- ] }),
1577
- /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", margin: "12px 0 0", padding: 0 }, children: entries.map((e) => /* @__PURE__ */ jsxs(
1578
- "li",
1579
- {
1580
- style: {
1581
- display: "flex",
1582
- flexWrap: "wrap",
1583
- gap: 8,
1584
- padding: "8px 0",
1585
- borderTop: `1px solid ${V.border}`,
1586
- fontFamily: SIGNIN_FONT,
1587
- fontSize: signinSize(11)
1588
- },
1589
- children: [
1590
- /* @__PURE__ */ jsx("code", { style: { color: V.foreground, wordBreak: "break-all" }, children: e.did_oc }),
1591
- /* @__PURE__ */ jsxs("span", { style: { ...hintStyle, marginLeft: "auto" }, children: [
1592
- "created ",
1593
- new Date(e.created_at).toLocaleDateString()
1594
- ] })
1595
- ]
1596
- },
1597
- e.did_oc
1598
- )) })
1599
- ] });
1600
- }
1601
- function AttestationLink() {
1602
- return /* @__PURE__ */ jsxs("div", { style: panelStyle(), "data-oc-li-attest": "", children: [
1603
- /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 portable attestation" }),
1604
- /* @__PURE__ */ jsx("p", { style: bodyStyle, children: "Make your identity links portable: publish a standard OrangeCheck attestation \u2014 one Bitcoin-wallet signature (BIP-322) binding your address to your handles (Nostr, GitHub, \u2026). It is published to Nostr and verifies with zero trust in OrangeCheck, so it outlives any single service." }),
1605
- /* @__PURE__ */ jsx(
1606
- "a",
1607
- {
1608
- href: "https://attest.ochk.io/create",
1609
- target: "_blank",
1610
- rel: "noreferrer",
1611
- style: { ...primaryChipStyle, textDecoration: "none", display: "inline-block" },
1612
- "data-oc-li-attest-link": "",
1613
- children: "open the attestation builder \u2197"
1614
- }
1615
- )
1616
- ] });
1617
- }
1618
- var ACCOUNT_WAYS_URL = "https://ochk.app/me/account#ways";
1619
- function LinkPromptStep({
1620
- method,
1621
- didOc,
1622
- authOrigin,
1623
- onResolved
1624
- }) {
1625
- const what = method === "btc" ? "a Bitcoin address" : "an email";
1626
- return /* @__PURE__ */ jsxs("div", { "data-oc-linkprompt": method, children: [
1627
- /* @__PURE__ */ jsxs("div", { style: { ...panelStyle("success"), marginBottom: 16 }, children: [
1628
- /* @__PURE__ */ jsx(SectionLabel, { tone: "success", children: "\xA7 signed in" }),
1629
- /* @__PURE__ */ jsxs("p", { style: { ...bodyStyle, margin: 0 }, children: [
1630
- "You chose to also link ",
1631
- what,
1632
- ". Complete it below \u2014 or skip for now; it is entirely optional and changes nothing about the sign-in you just completed."
1633
- ] })
1634
- ] }),
1635
- method === "btc" ? /* @__PURE__ */ jsx(
1636
- BtcLinkForm,
1637
- {
1638
- authOrigin,
1639
- didOc,
1640
- onDone: onResolved,
1641
- onCancel: onResolved
1642
- }
1643
- ) : /* @__PURE__ */ jsx(EmailLinkForm, { authOrigin, onDone: onResolved, onCancel: onResolved }),
1644
- /* @__PURE__ */ jsxs(
1645
- "div",
1646
- {
1647
- "data-oc-linkprompt-skip": "",
1648
- style: { marginTop: 14, paddingTop: 14, borderTop: `1px solid ${V.border}` },
1649
- children: [
1650
- /* @__PURE__ */ jsx("button", { type: "button", onClick: onResolved, style: ghostBtnStyle(false), children: "skip \u2014 I'll do this later" }),
1651
- /* @__PURE__ */ jsxs("p", { style: { ...hintStyle, marginTop: 8 }, children: [
1652
- "Changed your mind? Skipping continues you straight on \u2014 your sign-in is unaffected. You can link ",
1653
- what,
1654
- " anytime later from your account page,",
1655
- " ",
1656
- /* @__PURE__ */ jsx(
1657
- "a",
1658
- {
1659
- href: ACCOUNT_WAYS_URL,
1660
- target: "_blank",
1661
- rel: "noreferrer",
1662
- style: { color: "inherit" },
1663
- "data-oc-linkprompt-account": "",
1664
- children: "ochk.app/me/account"
1665
- }
1666
- ),
1667
- "."
1668
- ] })
1669
- ]
1670
- }
1671
- )
1672
- ] });
1673
- }
1674
- function EmailLinkForm({
1675
- authOrigin,
1676
- onDone,
1677
- onCancel
1678
- }) {
1679
- const [step, setStep] = React4.useState("email");
1680
- const [email, setEmail] = React4.useState("");
1681
- const [code, setCode] = React4.useState("");
1682
- const [token, setToken] = React4.useState("");
1683
- const [busy, setBusy] = React4.useState(false);
1684
- const [err, setErr] = React4.useState(null);
1685
- const [transfer, setTransfer] = React4.useState(false);
1686
- async function start() {
1687
- setBusy(true);
1688
- setErr(null);
1689
- try {
1690
- const r = await fetch(`${authOrigin}/api/auth/email-otp/start`, {
1691
- method: "POST",
1692
- credentials: "include",
1693
- headers: { "Content-Type": "application/json" },
1694
- body: JSON.stringify({ email: email.trim().toLowerCase() })
1695
- });
1696
- const j = await r.json();
1697
- if (!r.ok || !j.token) throw new Error(j.reason ?? `http_${r.status}`);
1698
- setToken(j.token);
1699
- setStep("code");
1700
- } catch (e) {
1701
- setErr(e instanceof Error ? e.message : "could not send code");
1702
- } finally {
1703
- setBusy(false);
1704
- }
1705
- }
1706
- async function verify(confirmTransfer) {
1707
- setBusy(true);
1708
- setErr(null);
1709
- try {
1710
- const r = await fetch(`${authOrigin}/api/auth/link/email`, {
1711
- method: "POST",
1712
- credentials: "include",
1713
- headers: { "Content-Type": "application/json" },
1714
- body: JSON.stringify({
1715
- email: email.trim().toLowerCase(),
1716
- code: code.trim(),
1717
- token,
1718
- ...confirmTransfer ? { confirm_transfer: true } : {}
1719
- })
1720
- });
1721
- const j = await r.json();
1722
- if (r.status === 409 && j.reason === "email_linked_elsewhere") {
1723
- setTransfer(true);
1724
- setBusy(false);
1725
- return;
1726
- }
1727
- if (handleSudoRequired(j, { purpose: "link an email" })) return;
1728
- if (isSudoAccountMismatch(j)) {
1729
- setErr(SUDO_ACCOUNT_MISMATCH_MESSAGE);
1730
- setBusy(false);
1731
- return;
1732
- }
1733
- if (!r.ok || !j.ok) throw new Error(j.reason ?? `http_${r.status}`);
1734
- onDone();
1735
- } catch (e) {
1736
- setErr(e instanceof Error ? e.message : "link failed");
1737
- setBusy(false);
1738
- }
1739
- }
1740
- return /* @__PURE__ */ jsxs("div", { style: panelStyle("accent"), "data-oc-li-form": "email", children: [
1741
- /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 link an email" }),
1742
- step === "email" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1743
- /* @__PURE__ */ jsx(
1744
- "input",
1745
- {
1746
- type: "email",
1747
- autoComplete: "email",
1748
- value: email,
1749
- onChange: (e) => setEmail(e.target.value),
1750
- placeholder: "you@example.com",
1751
- style: inputStyle
1752
- }
1753
- ),
1754
- err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
1755
- /* @__PURE__ */ jsxs(FormButtons, { children: [
1756
- /* @__PURE__ */ jsx(
1757
- "button",
1758
- {
1759
- type: "button",
1760
- onClick: () => void start(),
1761
- disabled: busy || email.trim().length < 5,
1762
- style: primaryBtnStyle(busy || email.trim().length < 5),
1763
- children: busy ? "sending\u2026" : "send code"
1764
- }
1765
- ),
1766
- /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, style: ghostBtnStyle(busy), children: "cancel" })
1767
- ] })
1768
- ] }) : transfer ? /* @__PURE__ */ jsx(
1769
- TransferConfirm,
1770
- {
1771
- what: "email",
1772
- busy,
1773
- onConfirm: () => void verify(true),
1774
- onCancel,
1775
- err
1776
- }
1777
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1778
- /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1779
- "Enter the 6-digit code sent to ",
1780
- email,
1781
- "."
1782
- ] }),
1783
- /* @__PURE__ */ jsx(
1784
- "input",
1785
- {
1786
- type: "text",
1787
- inputMode: "numeric",
1788
- autoComplete: "one-time-code",
1789
- maxLength: 6,
1790
- value: code,
1791
- onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
1792
- placeholder: "123456",
1793
- style: { ...inputStyle, letterSpacing: "0.3em" }
1794
- }
1795
- ),
1796
- err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
1797
- /* @__PURE__ */ jsxs(FormButtons, { children: [
1798
- /* @__PURE__ */ jsx(
1799
- "button",
1800
- {
1801
- type: "button",
1802
- onClick: () => void verify(false),
1803
- disabled: busy || code.length !== 6,
1804
- style: primaryBtnStyle(busy || code.length !== 6),
1805
- children: busy ? "linking\u2026" : "verify + link"
1806
- }
1807
- ),
1808
- /* @__PURE__ */ jsx(
1809
- "button",
1810
- {
1811
- type: "button",
1812
- onClick: () => setStep("email"),
1813
- disabled: busy,
1814
- style: ghostBtnStyle(busy),
1815
- children: "back"
1816
- }
1817
- )
1818
- ] })
1819
- ] })
1820
- ] });
1821
- }
1822
- function BtcLinkForm({
1823
- authOrigin,
1824
- didOc,
1825
- onDone,
1826
- onCancel
1827
- }) {
1828
- const [step, setStep] = React4.useState("request");
1829
- const [addr, setAddr] = React4.useState("");
1830
- const [challenge, setChallenge] = React4.useState("");
1831
- const [busy, setBusy] = React4.useState(false);
1832
- const [err, setErr] = React4.useState(null);
1833
- const [pendingSig, setPendingSig] = React4.useState(null);
1834
- async function getChallenge() {
1835
- if (!didOc) {
1836
- setErr("session not loaded yet \xB7 refresh and retry");
1837
- return;
1838
- }
1839
- setBusy(true);
1840
- setErr(null);
1841
- try {
1842
- const r = await fetch(
1843
- `${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&purpose=link&audience=${encodeURIComponent(didOc)}`
1844
- );
1845
- const j = await r.json();
1846
- if (!r.ok || !j.message) throw new Error(j.reason ?? `http_${r.status}`);
1847
- setChallenge(j.message);
1848
- setStep("sign");
1849
- } catch (e) {
1850
- setErr(e instanceof Error ? e.message : "challenge failed");
1851
- } finally {
1852
- setBusy(false);
1853
- }
1854
- }
1855
- async function postLink(sig, confirmTransfer) {
1856
- setBusy(true);
1857
- setErr(null);
1858
- try {
1859
- const r = await fetch(`${authOrigin}/api/auth/link/btc`, {
1860
- method: "POST",
1861
- credentials: "include",
1862
- headers: { "Content-Type": "application/json" },
1863
- body: JSON.stringify({
1864
- message: challenge,
1865
- signature: sig,
1866
- ...confirmTransfer ? { confirm_transfer: true } : {}
1867
- })
1868
- });
1869
- const j = await r.json();
1870
- if (r.status === 409 && j.reason === "address_linked_elsewhere") {
1871
- setPendingSig(sig);
1872
- setBusy(false);
1873
- return;
1874
- }
1875
- if (handleSudoRequired(j, { purpose: "link a Bitcoin address" })) return;
1876
- if (isSudoAccountMismatch(j)) {
1877
- setErr(SUDO_ACCOUNT_MISMATCH_MESSAGE);
1878
- setBusy(false);
1879
- return;
1880
- }
1881
- if (!r.ok || !j.ok) throw new Error(j.reason ?? `http_${r.status}`);
1882
- onDone();
1883
- } catch (e) {
1884
- setErr(e instanceof Error ? e.message : "link failed");
1885
- setBusy(false);
1886
- }
1887
- }
1888
- return /* @__PURE__ */ jsxs("div", { style: panelStyle("accent"), "data-oc-li-form": "btc", children: [
1889
- /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 link a Bitcoin address" }),
1890
- step === "request" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1891
- /* @__PURE__ */ jsx("p", { style: bodyStyle, children: "Enter a Bitcoin address you control. We generate a challenge and your wallet signs it (BIP-322)." }),
1892
- /* @__PURE__ */ jsx(
1893
- "input",
1894
- {
1895
- type: "text",
1896
- value: addr,
1897
- onChange: (e) => setAddr(e.target.value.trim()),
1898
- placeholder: "bc1q\u2026",
1899
- autoCapitalize: "none",
1900
- spellCheck: false,
1901
- style: { ...inputStyle, fontFamily: SIGNIN_FONT }
1902
- }
1903
- ),
1904
- err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
1905
- /* @__PURE__ */ jsxs(FormButtons, { children: [
1906
- /* @__PURE__ */ jsx(
1907
- "button",
1908
- {
1909
- type: "button",
1910
- onClick: () => void getChallenge(),
1911
- disabled: busy || addr.length < 14,
1912
- style: primaryBtnStyle(busy || addr.length < 14),
1913
- children: busy ? "requesting\u2026" : "get challenge"
1914
- }
1915
- ),
1916
- /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, style: ghostBtnStyle(busy), children: "cancel" })
1917
- ] })
1918
- ] }) : pendingSig ? /* @__PURE__ */ jsx(
1919
- TransferConfirm,
1920
- {
1921
- what: "bitcoin address",
1922
- busy,
1923
- onConfirm: () => void postLink(pendingSig, true),
1924
- onCancel,
1925
- err
1926
- }
1927
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1928
- /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1929
- "Sign the challenge with the wallet that controls",
1930
- " ",
1931
- /* @__PURE__ */ jsx("span", { style: { fontFamily: SIGNIN_FONT, wordBreak: "break-all" }, children: addr }),
1932
- "."
1933
- ] }),
1934
- /* @__PURE__ */ jsx(
1935
- LazyWalletButton,
1936
- {
1937
- address: addr,
1938
- message: challenge,
1939
- showManual: true,
1940
- layout: "list",
1941
- heading: null,
1942
- onSigned: (sig) => void postLink(sig, false),
1943
- onError: (e) => setErr(e?.message ?? "wallet rejected")
1944
- }
1945
- ),
1946
- err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
1947
- /* @__PURE__ */ jsx(FormButtons, { children: /* @__PURE__ */ jsx(
1948
- "button",
1949
- {
1950
- type: "button",
1951
- onClick: () => {
1952
- setStep("request");
1953
- setErr(null);
1954
- },
1955
- disabled: busy,
1956
- style: ghostBtnStyle(busy),
1957
- children: "back"
1958
- }
1959
- ) })
1960
- ] })
1961
- ] });
1962
- }
1963
- function TransferConfirm({
1964
- what,
1965
- busy,
1966
- onConfirm,
1967
- onCancel,
1968
- err
1969
- }) {
1970
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1971
- /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1972
- "This ",
1973
- what,
1974
- " is already linked to another OrangeCheck account. You've just proven control of it \u2014 you can transfer the link to this account under dual proof of control. The other account loses this sign-in path; its history stays verifiable. Continue?"
1975
- ] }),
1976
- err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
1977
- /* @__PURE__ */ jsxs(FormButtons, { children: [
1978
- /* @__PURE__ */ jsx(
1979
- "button",
1980
- {
1981
- type: "button",
1982
- onClick: onConfirm,
1983
- disabled: busy,
1984
- style: dangerBtnStyle(busy),
1985
- children: busy ? "transferring\u2026" : "transfer link here"
1986
- }
1987
- ),
1988
- /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, disabled: busy, style: ghostBtnStyle(busy), children: "cancel" })
1989
- ] })
1990
- ] });
1991
- }
1992
- function LazyWalletButton(props) {
1993
- const [Comp, setComp] = React4.useState(null);
1994
- const [failed, setFailed] = React4.useState(false);
1995
- React4.useEffect(() => {
1996
- let alive = true;
1997
- import('@orangecheck/wallet-adapter/react').then((m) => {
1998
- if (alive) {
1999
- setComp(
2000
- () => m.OcWalletButton
2001
- );
2002
- }
2003
- }).catch(() => {
2004
- if (alive) setFailed(true);
2005
- });
2006
- return () => {
2007
- alive = false;
2008
- };
2009
- }, []);
2010
- if (failed) {
2011
- return /* @__PURE__ */ jsx(ErrorLine, { children: "could not load wallet support \xB7 refresh to try again" });
2012
- }
2013
- if (!Comp) return /* @__PURE__ */ jsx("span", { style: hintStyle, children: "loading wallet options\u2026" });
2014
- return /* @__PURE__ */ jsx(Comp, { ...props });
2015
- }
2016
- function mask(value, kind) {
2017
- if (kind === "email") {
2018
- const [user, host] = value.split("@");
2019
- if (!user || !host) return "\u2022\u2022\u2022\u2022";
2020
- const m = user.length <= 2 ? "\u2022".repeat(user.length) : user[0] + "\u2022".repeat(user.length - 2) + user.slice(-1);
2021
- return `${m}@${host}`;
2022
- }
2023
- if (value.length <= 12) return "\u2022".repeat(value.length);
2024
- return `${value.slice(0, 6)}\u2026${value.slice(-4)}`;
2025
- }
2026
- function SectionLabel({
2027
- children,
2028
- tone = "primary"
2029
- }) {
2030
- const color = tone === "success" ? V.success : tone === "warn" ? V.warn : V.primary;
2031
- return /* @__PURE__ */ jsx(
2032
- "div",
2033
- {
2034
- style: {
2035
- color,
2036
- fontFamily: SIGNIN_FONT,
2037
- fontSize: signinSize(10),
2038
- letterSpacing: signinTracking("0.18em"),
2039
- textTransform: SIGNIN_CASE,
2040
- marginBottom: 8
2041
- },
2042
- children
2043
- }
2044
- );
2045
- }
2046
- function ErrorLine({ children }) {
2047
- return /* @__PURE__ */ jsx(
2048
- "p",
2049
- {
2050
- role: "alert",
2051
- style: {
2052
- margin: "8px 0 0",
2053
- color: V.destructive,
2054
- fontFamily: SIGNIN_FONT,
2055
- fontSize: signinSize(11)
2056
- },
2057
- children
2058
- }
2059
- );
2060
- }
2061
- function FormButtons({ children }) {
2062
- return /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, marginTop: 12 }, children });
2063
- }
2064
- var V = {
2065
- primary: "var(--primary, #f97316)",
2066
- foreground: "var(--foreground, #fafafa)",
2067
- muted: "var(--muted-foreground, #a1a1aa)",
2068
- background: "var(--background, #0a0a0a)",
2069
- card: "var(--card, #111113)",
2070
- border: "var(--border, #27272a)",
2071
- destructive: "var(--destructive, #ef4444)",
2072
- success: "var(--success, #22c55e)",
2073
- warn: "var(--warning, #eab308)"
2074
- };
2075
- function panelStyle(tone = "default") {
2076
- const base = {
2077
- background: V.card,
2078
- border: `1px solid ${V.border}`,
2079
- padding: "1.25rem"
2080
- };
2081
- if (tone === "plain") return { ...base, padding: 0 };
2082
- if (tone === "accent") return { ...base, borderColor: V.primary, background: "var(--card, #111113)" };
2083
- if (tone === "success") return { ...base, borderColor: V.success };
2084
- if (tone === "warn") return { ...base, borderColor: V.warn };
2085
- return base;
2086
- }
2087
- var bodyStyle = {
2088
- color: V.muted,
2089
- fontSize: 13,
2090
- lineHeight: 1.55,
2091
- margin: "0 0 12px"
2092
- };
2093
- var hintStyle = {
2094
- color: V.muted,
2095
- fontFamily: SIGNIN_FONT,
2096
- fontSize: signinSize(10.5),
2097
- opacity: 0.8
2098
- };
2099
- var rowStyle = {
2100
- display: "flex",
2101
- flexWrap: "wrap",
2102
- alignItems: "center",
2103
- justifyContent: "space-between",
2104
- gap: 12,
2105
- padding: "1rem"
2106
- };
2107
- var rowLabelStyle = {
2108
- color: V.foreground,
2109
- fontFamily: SIGNIN_FONT,
2110
- fontSize: signinSize(10.5),
2111
- letterSpacing: signinTracking("0.12em"),
2112
- textTransform: SIGNIN_CASE,
2113
- opacity: 0.8
2114
- };
2115
- var inputStyle = {
2116
- width: "100%",
2117
- padding: "0.55rem 0.75rem",
2118
- background: V.background,
2119
- color: V.foreground,
2120
- border: `1px solid ${V.border}`,
2121
- borderRadius: signinRadius(6),
2122
- fontFamily: SIGNIN_FONT,
2123
- // 16px — never lower · suppresses iOS Safari focus auto-zoom.
2124
- fontSize: 16,
2125
- outline: "none",
2126
- boxSizing: "border-box"
2127
- };
2128
- function btnBase(disabled) {
2129
- return {
2130
- padding: "0.5rem 0.875rem",
2131
- borderRadius: SIGNIN_BUTTON_RADIUS,
2132
- fontFamily: SIGNIN_FONT,
2133
- fontSize: signinSize(10.5),
2134
- letterSpacing: signinTracking("0.12em"),
2135
- textTransform: SIGNIN_CASE,
2136
- cursor: disabled ? "not-allowed" : "pointer",
2137
- opacity: disabled ? 0.5 : 1
2138
- };
2139
- }
2140
- function primaryBtnStyle(disabled) {
2141
- return {
2142
- ...btnBase(disabled),
2143
- background: V.primary,
2144
- color: "var(--primary-foreground, #0a0a0a)",
2145
- border: `1px solid ${V.primary}`,
2146
- fontWeight: 600
2147
- };
2148
- }
2149
- function ghostBtnStyle(disabled) {
2150
- return {
2151
- ...btnBase(disabled),
2152
- background: V.background,
2153
- color: V.foreground,
2154
- border: `1px solid ${V.border}`
2155
- };
2156
- }
2157
- function dangerBtnStyle(disabled) {
2158
- return {
2159
- ...btnBase(disabled),
2160
- background: "transparent",
2161
- color: V.destructive,
2162
- border: `1px solid ${V.destructive}`
2163
- };
2164
- }
2165
- var primaryChipStyle = {
2166
- ...btnBase(false),
2167
- background: "transparent",
2168
- color: V.primary,
2169
- border: `1px solid ${V.primary}`
2170
- };
2171
- function isSameOriginPath(candidate, origin) {
2172
- try {
2173
- return new URL(candidate, origin).origin === origin;
2174
- } catch {
2175
- return false;
2176
- }
2177
- }
2178
- function resolutionOrigin() {
2179
- return typeof window === "undefined" ? "https://ochk.io" : window.location.origin;
2180
- }
2181
- function safeReturnTo(input) {
2182
- const candidate = input ?? "/";
2183
- if (typeof candidate !== "string") return "/";
2184
- if (!candidate.startsWith("/")) return "/";
2185
- return isSameOriginPath(candidate, resolutionOrigin()) ? candidate : "/";
2186
- }
2187
- function familyReturnTarget(input) {
2188
- if (typeof input !== "string" || input.length === 0) return void 0;
2189
- if (input.startsWith("/") && isSameOriginPath(input, resolutionOrigin())) {
2190
- return input;
2191
- }
2192
- try {
2193
- const u = new URL(input);
2194
- if (u.protocol !== "https:") return void 0;
2195
- const host = u.hostname.toLowerCase();
2196
- if (host === "ochk.io" || host.endsWith(".ochk.io")) return u.toString();
2197
- } catch {
2198
- }
2199
- return void 0;
2200
- }
2201
- function hardNavigate(target) {
2202
- if (typeof window === "undefined") return;
2203
- window.location.assign(target);
2204
- }
2205
- var SIGNIN_ERRORS = {
2206
- bad_request: "That doesn't look like a Bitcoin address.",
2207
- invalid_challenge: "That signature doesn't match this address. Sign with the wallet that holds it.",
2208
- sig_invalid: "That signature doesn't match this address. Sign with the wallet that holds it.",
2209
- sig_unsupported_scheme: "We can't check this wallet's signature format yet. Try another wallet, or use email.",
2210
- expired: "That request timed out. Try again for a fresh one.",
2211
- not_yet_valid: "That request is not valid yet. Check your device clock, then try again.",
2212
- nonce_mismatch: "That request didn't match. Try again for a fresh one.",
2213
- audience_mismatch: "That request didn't match this site. Reload the page and try again.",
2214
- purpose_mismatch: "That request didn't match. Try again for a fresh one.",
2215
- malformed: "That signature couldn't be read. Copy it again, whole.",
2216
- cross_site_blocked: "Your browser blocked this request. Reload the page and try again.",
2217
- rate_limited: "Too many tries. Wait a minute, then try again.",
2218
- invalid_email: "That email address doesn't look right.",
2219
- mailer_unavailable: "We couldn't send the email right now. Try again in a minute.",
2220
- mailer_error: "We couldn't send the email right now. Try again in a minute.",
2221
- wrong_code: "That code isn't right. Check the newest email from us.",
2222
- replay: "That code was already used. Send a new one.",
2223
- invalid_token: "That code is no longer valid. Send a new one.",
2224
- email_mismatch: "That code was sent to a different address. Send a new one."
2225
- };
2226
- var EMAIL_ERRORS = {
2227
- expired: "That code expired. Send a new one."
2228
- };
2229
- var SUBMIT_ERRORS = {
2230
- bad_request: "That signature couldn't be read. Copy it again, whole."
2231
- };
2232
- var NETWORK_ERROR = "We couldn't reach the server. Check your connection and try again.";
2233
- function humanSigninError(reason, path, step = "challenge") {
2234
- if (!/^[a-z0-9_]+$/.test(reason)) return reason;
2235
- if (path === "wallet" && step === "submit" && SUBMIT_ERRORS[reason]) return SUBMIT_ERRORS[reason];
2236
- if (path === "email" && EMAIL_ERRORS[reason]) return EMAIL_ERRORS[reason];
2237
- return SIGNIN_ERRORS[reason] ?? (path === "email" ? "That didn't work. Try again, or send a new code." : "That didn't work. Try again.");
2238
- }
2239
- function failureMessage(err, path, step) {
2240
- if (err instanceof TypeError || err instanceof SyntaxError) return NETWORK_ERROR;
2241
- return humanSigninError(err instanceof Error ? err.message : "failed", path, step);
2242
- }
2243
- var BECH32_ADDRESS = /^(bc1|tb1|bcrt1)[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{8,87}$/i;
2244
- var BASE58_ADDRESS = /^[123mn][1-9A-HJ-NP-Za-km-z]{25,34}$/;
2245
- function looksLikeBitcoinAddress(input) {
2246
- const s = input.trim();
2247
- return BECH32_ADDRESS.test(s) || BASE58_ADDRESS.test(s);
2248
- }
2249
- function OcSignIn({
2250
- audience,
2251
- returnTo,
2252
- onSuccess,
2253
- resolveReturnTo,
2254
- linkPrompt: linkPromptProp = true,
2255
- add: addProp,
2256
- authOrigin = "https://ochk.io",
2257
- initialPath,
2258
- providersFirst = true,
2259
- paths,
2260
- className,
2261
- mode,
2262
- providerHref
2263
- }) {
2264
- const walletEnabled = paths?.wallet ?? true;
2265
- const emailEnabled = paths?.email ?? true;
2266
- const firstParty = mode === "first-party";
2267
- const credentials = firstParty ? "omit" : "include";
2268
- const linkPrompt = linkPromptProp && !firstParty;
2269
- const [resolvedReturn, setResolvedReturn] = React4.useState(
2270
- () => familyReturnTarget(returnTo) ?? safeReturnTo(returnTo)
2271
- );
2272
- React4.useEffect(() => {
2273
- const fromProp = familyReturnTarget(returnTo);
2274
- if (fromProp) {
2275
- setResolvedReturn(fromProp);
2276
- return;
2277
- }
2278
- if (typeof window === "undefined") return;
2279
- const q = new URLSearchParams(window.location.search);
2280
- const fromQuery = familyReturnTarget(q.get("return_to")) ?? familyReturnTarget(q.get("next"));
2281
- setResolvedReturn(fromQuery ?? "/");
2282
- }, [returnTo]);
2283
- const [addMode, setAddMode] = React4.useState(Boolean(addProp) && !firstParty);
2284
- React4.useEffect(() => {
2285
- if (typeof window === "undefined") return;
2286
- if (firstParty) {
2287
- setAddMode(false);
2288
- return;
2289
- }
2290
- if (addProp !== void 0) {
2291
- setAddMode(Boolean(addProp));
2292
- return;
2293
- }
2294
- setAddMode(new URLSearchParams(window.location.search).get("add") === "1");
2295
- }, [addProp, firstParty]);
2296
- const [path, setPath] = React4.useState(
2297
- initialPath ?? (providersFirst ? "email" : "wallet")
2298
- );
2299
- const [signedIn, setSignedIn] = React4.useState(null);
2300
- const [linkAlso, setLinkAlso] = React4.useState(false);
2301
- const [oauthError, setOauthError] = React4.useState(false);
2302
- React4.useEffect(() => {
2303
- if (typeof window === "undefined") return;
2304
- if (new URLSearchParams(window.location.search).has("oauth_error")) {
2305
- setOauthError(true);
2306
- }
2307
- }, []);
2308
- const navigate = React4.useCallback(
2309
- async (account) => {
2310
- if (resolveReturnTo) {
2311
- try {
2312
- const resolved = familyReturnTarget(await resolveReturnTo(account));
2313
- hardNavigate(resolved ?? resolvedReturn);
2314
- return;
2315
- } catch {
2316
- }
2317
- }
2318
- hardNavigate(resolvedReturn);
2319
- },
2320
- [resolveReturnTo, resolvedReturn]
2321
- );
2322
- const handleSuccess = React4.useCallback(
2323
- async (account, token, via) => {
2324
- const proceed = () => {
2325
- clearTabSession();
2326
- if (onSuccess) onSuccess(account, token);
2327
- else void navigate(account);
2328
- };
2329
- if (!linkPrompt || !linkAlso) {
2330
- proceed();
2331
- return;
2332
- }
2333
- try {
2334
- const meRes = await fetch(`${authOrigin}/api/auth/me`, {
2335
- credentials: "include",
2336
- headers: { Accept: "application/json" }
2337
- });
2338
- const me = meRes.ok ? await meRes.json() : null;
2339
- const didOc = me?.account?.did_oc;
2340
- if (didOc) {
2341
- setSignedIn({
2342
- method: via === "email" ? "btc" : "email",
2343
- didOc,
2344
- proceed
2345
- });
2346
- return;
2347
- }
2348
- } catch {
2349
- }
2350
- proceed();
2351
- },
2352
- [onSuccess, linkPrompt, linkAlso, navigate, authOrigin]
2353
- );
2354
- if (!walletEnabled && !emailEnabled) {
2355
- return /* @__PURE__ */ jsxs(
2356
- "div",
2357
- {
2358
- className,
2359
- "data-oc-signin": "",
2360
- style: {
2361
- padding: "1rem",
2362
- border: "1px solid var(--border, #27272a)",
2363
- color: "var(--foreground, #fafafa)",
2364
- fontFamily: SIGNIN_FONT,
2365
- fontSize: signinSize(12)
2366
- },
2367
- children: [
2368
- "no signin paths enabled \u2014 set ",
2369
- /* @__PURE__ */ jsx("code", { children: "paths.wallet" }),
2370
- " or",
2371
- " ",
2372
- /* @__PURE__ */ jsx("code", { children: "paths.email" }),
2373
- " to ",
2374
- /* @__PURE__ */ jsx("code", { children: "true" })
2375
- ]
2376
- }
2377
- );
2378
- }
2379
- if (signedIn) {
2380
- return /* @__PURE__ */ jsx("div", { className, "data-oc-signin": "", children: /* @__PURE__ */ jsx(
2381
- LinkPromptStep,
2382
- {
2383
- method: signedIn.method,
2384
- didOc: signedIn.didOc,
2385
- authOrigin,
2386
- onResolved: signedIn.proceed
2387
- }
2388
- ) });
2389
- }
2390
- const showWallet = walletEnabled && (path === "wallet" || !emailEnabled);
2391
- const showEmail = emailEnabled && (path === "email" || !walletEnabled);
2392
- const bothEnabled = walletEnabled && emailEnabled;
2393
- const activeMethod = !emailEnabled ? "wallet" : !walletEnabled ? "email" : path;
2394
- return /* @__PURE__ */ jsxs("div", { className, "data-oc-signin": "", children: [
2395
- oauthError && /* @__PURE__ */ jsx(
2396
- "div",
2397
- {
2398
- "data-oc-signin-oauth-error": "",
2399
- style: {
2400
- marginBottom: 14,
2401
- padding: "0.6rem 0.75rem",
2402
- border: "1px solid var(--destructive, #ef4444)",
2403
- borderRadius: signinRadius(6),
2404
- color: "var(--destructive, #ef4444)",
2405
- fontFamily: SIGNIN_FONT,
2406
- fontSize: signinRoleSize("text", signinSize(11))
2407
- },
2408
- children: "That sign-in didn't finish. Try again, or use another way below."
2409
- }
2410
- ),
2411
- addMode && /* @__PURE__ */ jsxs(
2412
- "div",
2413
- {
2414
- "data-oc-signin-add-mode": "",
2415
- style: {
2416
- marginBottom: 14,
2417
- padding: "0.6rem 0.75rem",
2418
- border: "1px solid var(--primary, #f97316)",
2419
- borderLeftWidth: 3,
2420
- borderRadius: signinRadius(4),
2421
- background: "color-mix(in srgb, var(--primary, #f97316) 8%, transparent)",
2422
- color: "var(--foreground, #fafafa)",
2423
- fontFamily: SIGNIN_FONT,
2424
- fontSize: signinRoleSize("text", signinSize(11)),
2425
- lineHeight: 1.6
2426
- },
2427
- children: [
2428
- /* @__PURE__ */ jsx(
2429
- "strong",
2430
- {
2431
- style: {
2432
- color: "var(--primary, #f97316)",
2433
- letterSpacing: signinTracking("0.12em"),
2434
- textTransform: SIGNIN_CASE,
2435
- fontSize: signinSize(10),
2436
- display: "block",
2437
- marginBottom: 3
2438
- },
2439
- children: "\xA7 adding another account"
2440
- }
2441
- ),
2442
- "Your current OrangeCheck account stays signed in. The new account joins your browser's roster; you can switch between them anytime from the account menu."
2443
- ]
2444
- }
2445
- ),
2446
- providersFirst && /* @__PURE__ */ jsx(
2447
- ProviderSignIn,
2448
- {
2449
- authOrigin,
2450
- returnTo: resolvedReturn,
2451
- add: addMode,
2452
- credentials,
2453
- firstParty,
2454
- providerHref,
2455
- first: true
2456
- }
2457
- ),
2458
- bothEnabled && /* @__PURE__ */ jsx(
2459
- "div",
2460
- {
2461
- role: "tablist",
2462
- "aria-label": "signin method",
2463
- "data-oc-signin-tabs": "",
2464
- style: {
2465
- display: "flex",
2466
- gap: 8,
2467
- marginBottom: 16,
2468
- borderBottom: "1px solid var(--border, #27272a)"
2469
- },
2470
- children: providersFirst ? /* @__PURE__ */ jsxs(Fragment, { children: [
2471
- /* @__PURE__ */ jsx(
2472
- SigninTab,
2473
- {
2474
- active: path === "email",
2475
- onClick: () => setPath("email"),
2476
- children: "Email"
2477
- }
2478
- ),
2479
- /* @__PURE__ */ jsx(
2480
- SigninTab,
2481
- {
2482
- active: path === "wallet",
2483
- onClick: () => setPath("wallet"),
2484
- children: "Bitcoin wallet"
2485
- }
2486
- )
2487
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2488
- /* @__PURE__ */ jsx(
2489
- SigninTab,
2490
- {
2491
- active: path === "wallet",
2492
- onClick: () => setPath("wallet"),
2493
- children: "Bitcoin wallet"
2494
- }
2495
- ),
2496
- /* @__PURE__ */ jsx(
2497
- SigninTab,
2498
- {
2499
- active: path === "email",
2500
- onClick: () => setPath("email"),
2501
- children: "Email"
2502
- }
2503
- )
2504
- ] })
2505
- }
2506
- ),
2507
- /* @__PURE__ */ jsxs("div", { "data-oc-signin-panel": "", role: "tabpanel", children: [
2508
- showWallet && /* @__PURE__ */ jsx(
2509
- WalletFlow,
2510
- {
2511
- authOrigin,
2512
- audience,
2513
- add: addMode,
2514
- credentials,
2515
- onSuccess: (a, t) => void handleSuccess(a, t, "wallet")
2516
- }
2517
- ),
2518
- showEmail && /* @__PURE__ */ jsx(
2519
- EmailFlow,
2520
- {
2521
- authOrigin,
2522
- add: addMode,
2523
- credentials,
2524
- onSuccess: (a, t) => void handleSuccess(a, t, "email")
2525
- }
2526
- )
2527
- ] }),
2528
- !providersFirst && /* @__PURE__ */ jsx(
2529
- ProviderSignIn,
2530
- {
2531
- authOrigin,
2532
- returnTo: resolvedReturn,
2533
- add: addMode,
2534
- credentials,
2535
- firstParty,
2536
- providerHref
2537
- }
2538
- ),
2539
- linkPrompt && /* @__PURE__ */ jsxs("label", { "data-oc-signin-linkalso": "", style: linkAlsoStyle, children: [
2540
- /* @__PURE__ */ jsx(
2541
- "input",
2542
- {
2543
- type: "checkbox",
2544
- checked: linkAlso,
2545
- onChange: (e) => setLinkAlso(e.target.checked),
2546
- style: { margin: 0, accentColor: "var(--primary, #f97316)" }
2547
- }
2548
- ),
2549
- /* @__PURE__ */ jsxs("span", { children: [
2550
- "After signing in, also link",
2551
- " ",
2552
- activeMethod === "email" ? "a Bitcoin wallet" : "an email",
2553
- " ",
2554
- /* @__PURE__ */ jsx("span", { children: "\u2014 optional, one more signature." })
2555
- ] })
2556
- ] })
2557
- ] });
2558
- }
2559
- function SigninTab({
2560
- active,
2561
- onClick,
2562
- children
2563
- }) {
2564
- return /* @__PURE__ */ jsx(
2565
- "button",
2566
- {
2567
- type: "button",
2568
- role: "tab",
2569
- "aria-selected": active,
2570
- onClick,
2571
- "data-oc-signin-tab": active ? "active" : "inactive",
2572
- style: {
2573
- minHeight: 44,
2574
- padding: "0.6rem 0.875rem",
2575
- background: "transparent",
2576
- border: "none",
2577
- borderBottom: active ? "2px solid var(--primary, #f97316)" : "2px solid transparent",
2578
- color: active ? "var(--foreground, #fafafa)" : "var(--muted-foreground, #a1a1aa)",
2579
- fontFamily: SIGNIN_FONT,
2580
- fontSize: signinRoleSize("label", signinSize(11)),
2581
- letterSpacing: signinTracking("0.12em"),
2582
- textTransform: SIGNIN_CASE,
2583
- cursor: "pointer",
2584
- marginBottom: -1
2585
- },
2586
- children
2587
- }
2588
- );
2589
- }
2590
- function ProviderIcon({ id }) {
2591
- const common = {
2592
- width: 14,
2593
- height: 14,
2594
- viewBox: "0 0 24 24",
2595
- fill: "currentColor",
2596
- "aria-hidden": true,
2597
- style: { flex: "0 0 auto" }
2598
- };
2599
- if (id === "google") {
2600
- return /* @__PURE__ */ jsx("svg", { ...common, children: /* @__PURE__ */ jsx("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" }) });
2601
- }
2602
- if (id === "github") {
2603
- return /* @__PURE__ */ jsx("svg", { ...common, children: /* @__PURE__ */ jsx("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" }) });
2604
- }
2605
- return null;
2606
- }
2607
- function ProviderSignIn({
2608
- authOrigin,
2609
- returnTo,
2610
- add,
2611
- credentials,
2612
- firstParty,
2613
- providerHref,
2614
- first = false
2615
- }) {
2616
- const [providers, setProviders] = React4.useState(null);
2617
- const [origin, setOrigin] = React4.useState("");
2618
- const hidden = firstParty && !providerHref;
2619
- React4.useEffect(() => {
2620
- setOrigin(window.location.origin);
2621
- if (hidden) return;
2622
- let cancelled = false;
2623
- fetch(`${authOrigin}/api/auth/providers`, { credentials }).then((r) => r.ok ? r.json() : null).then((body) => {
2624
- if (!cancelled) setProviders(body?.providers ?? []);
2625
- }).catch(() => {
2626
- if (!cancelled) setProviders([]);
2627
- });
2628
- return () => {
2629
- cancelled = true;
2630
- };
2631
- }, [authOrigin, credentials, hidden]);
2632
- if (hidden) return null;
2633
- if (providers !== null && providers.length === 0) return null;
2634
- const providerReturnTo = stripFragment(
2635
- returnTo.startsWith("/") ? origin ? `${origin}${returnTo}` : returnTo : returnTo
2636
- );
2637
- const line = { flex: 1, height: 1, background: "var(--border, #27272a)" };
2638
- const BLOCK_GAP = 16;
2639
- const BUTTON_GAP = 8;
2640
- const divider = /* @__PURE__ */ jsxs(
2641
- "div",
2642
- {
2643
- style: {
2644
- display: "flex",
2645
- alignItems: "center",
2646
- gap: 10,
2647
- // Symmetric in both orderings: in `first` mode the divider
2648
- // owns the gap on both sides; at the bottom the container's
2649
- // marginTop owns the gap above, so the divider only pads below.
2650
- margin: first ? `${BLOCK_GAP}px 0` : `0 0 ${BLOCK_GAP}px`,
2651
- color: "var(--muted-foreground, #a1a1aa)",
2652
- fontFamily: SIGNIN_FONT,
2653
- fontSize: signinRoleSize("hint", signinSize(10)),
2654
- letterSpacing: signinTracking("0.16em"),
2655
- textTransform: SIGNIN_CASE
2656
- },
2657
- children: [
2658
- /* @__PURE__ */ jsx("span", { style: line }),
2659
- "or",
2660
- /* @__PURE__ */ jsx("span", { style: line })
2661
- ]
2662
- }
2663
- );
2664
- const buttonStyle = (i) => ({
2665
- display: "flex",
2666
- alignItems: "center",
2667
- justifyContent: "center",
2668
- gap: 10,
2669
- boxSizing: "border-box",
2670
- width: "100%",
2671
- minHeight: 44,
2672
- marginTop: i === 0 ? 0 : BUTTON_GAP,
2673
- padding: "0.6rem 0.875rem",
2674
- border: "1px solid var(--border, #27272a)",
2675
- borderRadius: SIGNIN_BUTTON_RADIUS,
2676
- background: "transparent",
2677
- color: "var(--muted-foreground, #a1a1aa)",
2678
- fontFamily: SIGNIN_FONT,
2679
- fontSize: signinSize(12),
2680
- textDecoration: "none"
2681
- });
2682
- if (providers === null) {
2683
- if (!first) return null;
2684
- return /* @__PURE__ */ jsxs("div", { "data-oc-signin-providers": "", "aria-hidden": "true", children: [
2685
- [0, 1].map((i) => /* @__PURE__ */ jsx("div", { style: { ...buttonStyle(i), visibility: "hidden" }, children: "\xA0" }, i)),
2686
- divider
2687
- ] });
2688
- }
2689
- const buttons = providers.map((p, i) => /* @__PURE__ */ jsxs(
2690
- "a",
2691
- {
2692
- href: providerHref ? providerHref(p.id) : `${authOrigin}/api/auth/${p.id}/start?return_to=${encodeURIComponent(
2693
- providerReturnTo
2694
- )}${add ? "&add=1" : ""}`,
2695
- "data-oc-signin-provider": p.id,
2696
- style: buttonStyle(i),
2697
- children: [
2698
- /* @__PURE__ */ jsx(ProviderIcon, { id: p.id }),
2699
- /* @__PURE__ */ jsx("span", { children: p.label })
2700
- ]
2701
- },
2702
- p.id
2703
- ));
2704
- return /* @__PURE__ */ jsx(
2705
- "div",
2706
- {
2707
- "data-oc-signin-providers": "",
2708
- style: first ? { marginBottom: 0 } : { marginTop: BLOCK_GAP },
2709
- children: first ? /* @__PURE__ */ jsxs(Fragment, { children: [
2710
- buttons,
2711
- divider
2712
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2713
- divider,
2714
- buttons
2715
- ] })
2716
- }
2717
- );
2718
- }
2719
- async function loadAdapter() {
2720
- return await import('@orangecheck/wallet-adapter');
2721
- }
2722
- async function detectBrowserWallets() {
2723
- return (await loadAdapter()).detectWallets().filter((w) => w.detected && w.id !== "manual").map((w) => ({ id: w.id, name: w.name }));
2724
- }
2725
- function WalletFlow({
2726
- authOrigin,
2727
- audience,
2728
- add,
2729
- credentials,
2730
- onSuccess
2731
- }) {
2732
- const uid = React4.useId();
2733
- const ids = {
2734
- address: `${uid}-address`,
2735
- wallet: `${uid}-wallet`,
2736
- message: `${uid}-message`,
2737
- signature: `${uid}-signature`
2738
- };
2739
- const [address, setAddress] = React4.useState("");
2740
- const [error, setError] = React4.useState(null);
2741
- const [submitting, setSubmitting] = React4.useState(false);
2742
- const [wallets, setWallets] = React4.useState(null);
2743
- const [walletId, setWalletId] = React4.useState(null);
2744
- const [manual, setManual] = React4.useState(null);
2745
- const [pasted, setPasted] = React4.useState("");
2746
- const [copied, setCopied] = React4.useState("idle");
2747
- React4.useEffect(() => {
2748
- let cancelled = false;
2749
- detectBrowserWallets().then((found) => {
2750
- if (cancelled) return;
2751
- setWallets(found);
2752
- setWalletId(found[0]?.id ?? null);
2753
- }).catch(() => {
2754
- if (!cancelled) setWallets([]);
2755
- });
2756
- return () => {
2757
- cancelled = true;
2758
- };
2759
- }, []);
2760
- async function fetchChallenge(addr) {
2761
- const res = await fetch(
2762
- `${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&audience=${encodeURIComponent(audience)}&purpose=login`
2763
- );
2764
- const json = await res.json();
2765
- if (!res.ok || !json.message || !json.nonce) {
2766
- throw new Error(json.error ?? "challenge_failed");
2767
- }
2768
- return { message: json.message, nonce: json.nonce };
2769
- }
2770
- async function submitSignature(message, nonce, signature, scheme) {
2771
- const res = await fetch(`${authOrigin}/api/auth/signin`, {
2772
- method: "POST",
2773
- credentials,
2774
- headers: { "Content-Type": "application/json" },
2775
- body: JSON.stringify({
2776
- message,
2777
- signature,
2778
- ...scheme ? { scheme } : {},
2779
- expectedNonce: nonce,
2780
- expectedAudience: audience,
2781
- expectedPurpose: "login",
2782
- // Multi-account add-mode · the auth host preserves the current
2783
- // roster_id when set, instead of minting a fresh one.
2784
- ...add ? { add: true } : {}
2785
- })
2786
- });
2787
- const json = await res.json();
2788
- if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
2789
- throw new Error(
2790
- "reason" in json && json.reason || "error" in json && json.error || "verify_failed"
2791
- );
2792
- }
2793
- onSuccess(json.account, json.token);
2794
- }
2795
- async function signIn(e) {
2796
- e.preventDefault();
2797
- const addr = address.trim();
2798
- if (!addr) {
2799
- setError("Paste your Bitcoin address.");
2800
- return;
2801
- }
2802
- if (!looksLikeBitcoinAddress(addr)) {
2803
- setError(humanSigninError("bad_request", "wallet"));
2804
- return;
2805
- }
2806
- setError(null);
2807
- setSubmitting(true);
2808
- let step = "challenge";
2809
- try {
2810
- let id = walletId;
2811
- if (wallets === null) {
2812
- const found = await detectBrowserWallets().catch(() => []);
2813
- setWallets(found);
2814
- id = found[0]?.id ?? null;
2815
- setWalletId(id);
2816
- }
2817
- const challenge = await fetchChallenge(addr);
2818
- if (!id) {
2819
- setManual(challenge);
2820
- setSubmitting(false);
2821
- return;
2822
- }
2823
- const signer = (await loadAdapter()).getSigner(id, { address: addr });
2824
- const signature = await signer(challenge.message);
2825
- step = "submit";
2826
- await submitSignature(challenge.message, challenge.nonce, signature, "bip322");
2827
- } catch (err) {
2828
- setError(failureMessage(err, "wallet", step));
2829
- setSubmitting(false);
2830
- }
2831
- }
2832
- async function signInManual(e) {
2833
- e.preventDefault();
2834
- if (!manual) return;
2835
- const sig = pasted.trim();
2836
- if (!sig) {
2837
- setError("Paste the signature your wallet gave you.");
2838
- return;
2839
- }
2840
- setError(null);
2841
- setSubmitting(true);
2842
- try {
2843
- await submitSignature(manual.message, manual.nonce, sig, void 0);
2844
- } catch (err) {
2845
- setError(failureMessage(err, "wallet", "submit"));
2846
- setSubmitting(false);
2847
- }
2848
- }
2849
- async function copyMessage() {
2850
- if (!manual) return;
2851
- try {
2852
- await navigator.clipboard.writeText(manual.message);
2853
- setCopied("copied");
2854
- } catch {
2855
- setCopied("failed");
2856
- }
2857
- }
2858
- if (manual) {
2859
- return /* @__PURE__ */ jsxs("form", { onSubmit: signInManual, "data-oc-signin-wallet-manual": "", children: [
2860
- /* @__PURE__ */ jsxs(FlowHeader, { children: [
2861
- "In your wallet, use \u201Csign message\u201D (BIP-322) with ",
2862
- address.trim(),
2863
- ", sign the text below, and paste the signature back here. Nothing is spent."
2864
- ] }),
2865
- /* @__PURE__ */ jsx(Label, { htmlFor: ids.message, children: "Message to sign" }),
2866
- /* @__PURE__ */ jsx(
2867
- "textarea",
2868
- {
2869
- id: ids.message,
2870
- readOnly: true,
2871
- value: manual.message,
2872
- rows: 5,
2873
- onFocus: (e) => e.currentTarget.select(),
2874
- style: { ...inputStyle2, resize: "vertical" }
2875
- }
2876
- ),
2877
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void copyMessage(), style: linkButtonStyle, children: copied === "copied" ? "Copied" : "Copy message" }),
2878
- copied === "failed" && /* @__PURE__ */ jsx(Hint, { children: "Couldn't copy here. Select the text above and copy it." }),
2879
- /* @__PURE__ */ jsxs("div", { style: { marginTop: 14 }, children: [
2880
- /* @__PURE__ */ jsx(Label, { htmlFor: ids.signature, children: "Signature" }),
2881
- /* @__PURE__ */ jsx(
2882
- "textarea",
2883
- {
2884
- id: ids.signature,
2885
- value: pasted,
2886
- onChange: (e) => {
2887
- setPasted(e.target.value);
2888
- if (error) setError(null);
2889
- },
2890
- rows: 3,
2891
- placeholder: "paste the signature",
2892
- spellCheck: false,
2893
- autoFocus: true,
2894
- "aria-invalid": error ? true : void 0,
2895
- style: { ...inputStyle2, resize: "vertical" }
2896
- }
2897
- )
2898
- ] }),
2899
- error && /* @__PURE__ */ jsx(ErrorLine2, { children: error }),
2900
- /* @__PURE__ */ jsx(
2901
- "button",
2902
- {
2903
- type: "submit",
2904
- disabled: submitting || !pasted.trim(),
2905
- style: submitStyle(submitting || !pasted.trim()),
2906
- children: submitting ? "Checking\u2026" : /* @__PURE__ */ jsx(Submit, { children: "Sign me in" })
2907
- }
2908
- ),
2909
- /* @__PURE__ */ jsx(
2910
- "button",
2911
- {
2912
- type: "button",
2913
- onClick: () => {
2914
- setManual(null);
2915
- setPasted("");
2916
- setError(null);
2917
- setCopied("idle");
2918
- },
2919
- style: linkButtonStyle,
2920
- children: "Use a different address"
2921
- }
2922
- )
2923
- ] });
2924
- }
2925
- const noBrowserWallet = wallets !== null && wallets.length === 0;
2926
- return /* @__PURE__ */ jsxs("form", { onSubmit: signIn, "data-oc-signin-wallet": "", children: [
2927
- /* @__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." }),
2928
- /* @__PURE__ */ jsx(Label, { htmlFor: ids.address, children: "Bitcoin address" }),
2929
- /* @__PURE__ */ jsx(
2930
- "input",
2931
- {
2932
- id: ids.address,
2933
- type: "text",
2934
- value: address,
2935
- onChange: (e) => {
2936
- setAddress(e.target.value);
2937
- if (error) setError(null);
2938
- },
2939
- placeholder: "bc1q\u2026 \xB7 3\u2026 \xB7 1\u2026",
2940
- autoComplete: "off",
2941
- autoCapitalize: "none",
2942
- spellCheck: false,
2943
- required: true,
2944
- "aria-invalid": error ? true : void 0,
2945
- style: inputStyle2
2946
- }
2947
- ),
2948
- wallets && wallets.length > 1 && /* @__PURE__ */ jsxs("div", { style: { marginTop: 12 }, children: [
2949
- /* @__PURE__ */ jsx(Label, { htmlFor: ids.wallet, children: "Sign with" }),
2950
- /* @__PURE__ */ jsx(
2951
- "select",
2952
- {
2953
- id: ids.wallet,
2954
- value: walletId ?? "",
2955
- onChange: (e) => setWalletId(e.target.value),
2956
- style: inputStyle2,
2957
- children: wallets.map((w) => /* @__PURE__ */ jsx("option", { value: w.id, children: w.name }, w.id))
2958
- }
2959
- )
2960
- ] }),
2961
- error && /* @__PURE__ */ jsx(ErrorLine2, { children: error }),
2962
- /* @__PURE__ */ jsx(
2963
- "button",
2964
- {
2965
- type: "submit",
2966
- disabled: submitting || !address.trim(),
2967
- style: submitStyle(submitting || !address.trim()),
2968
- children: submitting ? "Waiting for wallet\u2026" : noBrowserWallet ? /* @__PURE__ */ jsx(Submit, { children: "Get the message to sign" }) : /* @__PURE__ */ jsx(Submit, { children: "Sign in with wallet" })
2969
- }
2970
- ),
2971
- noBrowserWallet && /* @__PURE__ */ jsx(Hint, { children: "No browser wallet found here. You can sign with any wallet and paste." })
2972
- ] });
2973
- }
2974
- function EmailFlow({ authOrigin, add, credentials, onSuccess }) {
2975
- const uid = React4.useId();
2976
- const [stage, setStage] = React4.useState("enter");
2977
- const [email, setEmail] = React4.useState("");
2978
- const [emailError, setEmailError] = React4.useState(null);
2979
- const [code, setCode] = React4.useState("");
2980
- const [codeError, setCodeError] = React4.useState(null);
2981
- const [token, setToken] = React4.useState(null);
2982
- const [submitting, setSubmitting] = React4.useState(false);
2983
- const [resent, setResent] = React4.useState(false);
2984
- async function sendCode(address) {
2985
- try {
2986
- const res = await fetch(`${authOrigin}/api/auth/email-otp/start`, {
2987
- method: "POST",
2988
- credentials,
2989
- headers: { "Content-Type": "application/json" },
2990
- body: JSON.stringify({ email: address })
2991
- });
2992
- const json = await res.json();
2993
- if (!res.ok || !json.token) return humanSigninError(json.reason ?? "server_error", "email");
2994
- setToken(json.token);
2995
- return null;
2996
- } catch {
2997
- return NETWORK_ERROR;
2998
- }
2999
- }
3000
- async function start(e) {
3001
- e.preventDefault();
3002
- const trimmed = email.trim().toLowerCase();
3003
- if (!trimmed || !/.+@.+\..+/.test(trimmed)) {
3004
- setEmailError(humanSigninError("invalid_email", "email"));
3005
- return;
3006
- }
3007
- setEmailError(null);
3008
- setSubmitting(true);
3009
- const err = await sendCode(trimmed);
3010
- setSubmitting(false);
3011
- if (err) {
3012
- setEmailError(err);
3013
- return;
3014
- }
3015
- setEmail(trimmed);
3016
- setStage("code");
3017
- }
3018
- async function resend() {
3019
- setCodeError(null);
3020
- setResent(false);
3021
- setSubmitting(true);
3022
- const err = await sendCode(email);
3023
- setSubmitting(false);
3024
- setCode("");
3025
- if (err) setCodeError(err);
3026
- else setResent(true);
3027
- }
3028
- async function verify(e) {
3029
- e.preventDefault();
3030
- if (!token) return;
3031
- if (code.length !== 6) {
3032
- setCodeError("The code is 6 digits.");
3033
- return;
3034
- }
3035
- setCodeError(null);
3036
- setSubmitting(true);
3037
- try {
3038
- const res = await fetch(`${authOrigin}/api/auth/email-otp/verify`, {
3039
- method: "POST",
3040
- credentials,
3041
- headers: { "Content-Type": "application/json" },
3042
- body: JSON.stringify({
3043
- email,
3044
- code,
3045
- token,
3046
- // Multi-account add-mode · see WalletFlow.
3047
- ...add ? { add: true } : {}
3048
- })
3049
- });
3050
- const json = await res.json();
3051
- if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
3052
- throw new Error(
3053
- "reason" in json && json.reason || "error" in json && json.error || "server_error"
3054
- );
3055
- }
3056
- onSuccess(json.account, json.token);
3057
- } catch (err) {
3058
- setCodeError(failureMessage(err, "email", "submit"));
3059
- } finally {
3060
- setSubmitting(false);
3061
- }
3062
- }
3063
- if (stage === "enter") {
3064
- return /* @__PURE__ */ jsxs("form", { onSubmit: start, "data-oc-signin-email": "", children: [
3065
- /* @__PURE__ */ jsx(FlowHeader, { children: "We email you a 6-digit code. No password, and no wallet needed to start. Add a Bitcoin address any time: a way in that only you hold." }),
3066
- /* @__PURE__ */ jsx(Label, { htmlFor: `${uid}-email`, children: "Email" }),
3067
- /* @__PURE__ */ jsx(
3068
- "input",
3069
- {
3070
- id: `${uid}-email`,
3071
- type: "email",
3072
- autoComplete: "email",
3073
- value: email,
3074
- onChange: (e) => {
3075
- setEmail(e.target.value);
3076
- if (emailError) setEmailError(null);
3077
- },
3078
- placeholder: "you@example.com",
3079
- required: true,
3080
- "aria-invalid": emailError ? true : void 0,
3081
- style: inputStyle2
3082
- }
3083
- ),
3084
- emailError && /* @__PURE__ */ jsx(ErrorLine2, { children: emailError }),
3085
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: submitting, style: submitStyle(submitting), children: submitting ? "Sending\u2026" : /* @__PURE__ */ jsx(Submit, { children: "Send one-time code" }) })
3086
- ] });
3087
- }
3088
- return /* @__PURE__ */ jsxs("form", { onSubmit: verify, "data-oc-signin-email-verify": "", children: [
3089
- /* @__PURE__ */ jsxs(FlowHeader, { children: [
3090
- resent ? "Sent a new code to " : "Sent a 6-digit code to ",
3091
- /* @__PURE__ */ jsx("span", { style: { color: "var(--foreground, #fafafa)" }, children: email }),
3092
- ". It expires in 10 minutes."
3093
- ] }),
3094
- /* @__PURE__ */ jsx(Label, { htmlFor: `${uid}-code`, children: "One-time code" }),
3095
- /* @__PURE__ */ jsx(
3096
- "input",
3097
- {
3098
- id: `${uid}-code`,
3099
- type: "text",
3100
- inputMode: "numeric",
3101
- pattern: "\\d{6}",
3102
- maxLength: 6,
3103
- autoComplete: "one-time-code",
3104
- value: code,
3105
- onChange: (e) => {
3106
- setCode(e.target.value.replace(/\D/g, "").slice(0, 6));
3107
- if (codeError) setCodeError(null);
3108
- },
3109
- placeholder: "6 digits",
3110
- required: true,
3111
- autoFocus: true,
3112
- "aria-invalid": codeError ? true : void 0,
3113
- style: { ...inputStyle2, letterSpacing: "0.4em", fontSize: 16 }
3114
- }
3115
- ),
3116
- codeError && /* @__PURE__ */ jsx(ErrorLine2, { children: codeError }),
3117
- /* @__PURE__ */ jsx(
3118
- "button",
3119
- {
3120
- type: "submit",
3121
- disabled: submitting || code.length !== 6,
3122
- style: submitStyle(submitting || code.length !== 6),
3123
- children: submitting ? "Verifying\u2026" : /* @__PURE__ */ jsx(Submit, { children: "Verify \xB7 sign me in" })
3124
- }
3125
- ),
3126
- /* @__PURE__ */ jsxs("div", { children: [
3127
- /* @__PURE__ */ jsx(
3128
- "button",
3129
- {
3130
- type: "button",
3131
- onClick: () => void resend(),
3132
- disabled: submitting,
3133
- style: linkButtonStyle,
3134
- children: "Send a new code"
3135
- }
3136
- ),
3137
- /* @__PURE__ */ jsx(
3138
- "button",
3139
- {
3140
- type: "button",
3141
- onClick: () => {
3142
- setStage("enter");
3143
- setCode("");
3144
- setToken(null);
3145
- setResent(false);
3146
- },
3147
- style: linkButtonStyle,
3148
- children: "Use a different email"
3149
- }
3150
- )
3151
- ] })
3152
- ] });
3153
- }
3154
- function Submit({ children }) {
3155
- return /* @__PURE__ */ jsxs(Fragment, { children: [
3156
- children,
3157
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { display: SIGNIN_ARROW }, children: " \u2192" })
3158
- ] });
3159
- }
3160
- function FlowHeader({
3161
- label,
3162
- children
3163
- }) {
3164
- return /* @__PURE__ */ jsxs("div", { style: { marginBottom: 16 }, children: [
3165
- label && /* @__PURE__ */ jsx(
3166
- "div",
3167
- {
3168
- style: {
3169
- color: "var(--primary, #f97316)",
3170
- fontFamily: SIGNIN_FONT,
3171
- fontSize: signinSize(10),
3172
- letterSpacing: signinTracking("0.18em"),
3173
- textTransform: SIGNIN_CASE,
3174
- marginBottom: 6
3175
- },
3176
- children: label
3177
- }
3178
- ),
3179
- /* @__PURE__ */ jsx(
3180
- "p",
3181
- {
3182
- style: {
3183
- color: "var(--muted-foreground, #a1a1aa)",
3184
- fontSize: signinRoleSize("text", "13px"),
3185
- lineHeight: 1.55,
3186
- margin: 0
3187
- },
3188
- children
3189
- }
3190
- )
3191
- ] });
3192
- }
3193
- var linkButtonStyle = {
3194
- display: "inline-flex",
3195
- alignItems: "center",
3196
- minHeight: 44,
3197
- marginTop: 4,
3198
- marginRight: 16,
3199
- background: "transparent",
3200
- border: "none",
3201
- color: "var(--muted-foreground, #a1a1aa)",
3202
- fontFamily: SIGNIN_FONT,
3203
- fontSize: signinRoleSize("button", signinSize(11)),
3204
- letterSpacing: signinTracking("0.12em"),
3205
- textTransform: SIGNIN_CASE,
3206
- cursor: "pointer",
3207
- padding: 0
3208
- };
3209
- function Label({
3210
- htmlFor,
3211
- children
3212
- }) {
3213
- return /* @__PURE__ */ jsx(
3214
- "label",
3215
- {
3216
- htmlFor,
3217
- style: {
3218
- display: "block",
3219
- color: "var(--foreground, #fafafa)",
3220
- fontFamily: SIGNIN_FONT,
3221
- fontSize: signinRoleSize("label", signinSize(11)),
3222
- letterSpacing: signinTracking("0.12em"),
3223
- textTransform: SIGNIN_CASE,
3224
- marginBottom: 6
3225
- },
3226
- children
3227
- }
3228
- );
3229
- }
3230
- var inputStyle2 = {
3231
- width: "100%",
3232
- padding: "0.55rem 0.75rem",
3233
- background: "var(--background, #0a0a0a)",
3234
- color: "var(--foreground, #fafafa)",
3235
- border: "1px solid var(--input, #27272a)",
3236
- borderRadius: signinRadius(6),
3237
- fontFamily: SIGNIN_FONT,
3238
- // 16px — never below it. A sub-16px font-size makes iOS Safari
3239
- // auto-zoom the viewport on focus; the family rule is >=16px on
3240
- // every form field. Desktop reads fine at 16 too.
3241
- fontSize: 16,
3242
- outline: "none"
3243
- };
3244
- var linkAlsoStyle = {
3245
- display: "flex",
3246
- alignItems: "center",
3247
- // The whole row is the checkbox's hit area; the 13px box alone was not.
3248
- minHeight: 44,
3249
- gap: 8,
3250
- marginTop: 16,
3251
- paddingTop: 14,
3252
- borderTop: "1px solid var(--border, #27272a)",
3253
- color: "var(--muted-foreground, #a1a1aa)",
3254
- fontFamily: SIGNIN_FONT,
3255
- fontSize: signinRoleSize("text", signinSize(12)),
3256
- lineHeight: 1.5,
3257
- cursor: "pointer"
3258
- };
3259
- function submitStyle(disabled) {
3260
- return {
3261
- marginTop: 12,
3262
- padding: "0.6rem 1rem",
3263
- background: "var(--primary, #f97316)",
3264
- color: "var(--primary-foreground, #0a0a0a)",
3265
- border: "none",
3266
- borderRadius: SIGNIN_BUTTON_RADIUS,
3267
- fontFamily: SIGNIN_FONT,
3268
- fontSize: signinRoleSize("button", signinSize(11)),
3269
- letterSpacing: signinTracking("0.14em"),
3270
- textTransform: SIGNIN_CASE,
3271
- fontWeight: SIGNIN_STRONG,
3272
- cursor: disabled ? "not-allowed" : "pointer",
3273
- opacity: disabled ? 0.5 : 1,
3274
- width: "100%"
3275
- };
3276
- }
3277
- function ErrorLine2({ children }) {
3278
- return /* @__PURE__ */ jsx(
3279
- "p",
3280
- {
3281
- role: "alert",
3282
- style: {
3283
- marginTop: 6,
3284
- marginBottom: 0,
3285
- color: "var(--destructive, #ef4444)",
3286
- fontFamily: SIGNIN_FONT,
3287
- fontSize: signinRoleSize("hint", signinSize(11)),
3288
- letterSpacing: "0.04em"
3289
- },
3290
- children
3291
- }
3292
- );
3293
- }
3294
- function Hint({ children }) {
3295
- return /* @__PURE__ */ jsx(
3296
- "p",
3297
- {
3298
- style: {
3299
- marginTop: 12,
3300
- marginBottom: 0,
3301
- color: "var(--muted-foreground, #a1a1aa)",
3302
- fontFamily: SIGNIN_FONT,
3303
- fontSize: signinRoleSize("hint", "10.5px"),
3304
- lineHeight: 1.55
3305
- },
3306
- children
3307
- }
3308
- );
3309
- }
3310
- var DEFAULT_AUTH_ORIGIN2 = "https://ochk.io";
3311
- var MIN_CONFIRMATIONS = 6;
3312
- var MIN_SATS = 1e3;
3313
- var n = (v) => v.toLocaleString("en-US");
3314
- function bondFloorsText(f) {
3315
- const sats = f.sats_floor > 0 ? `at least ${n(f.sats_floor)} sats` : "under 1,000 sats";
3316
- const days = f.days_floor > 0 ? `held ${f.days_floor} days or more` : "held under 7 days";
3317
- return `${sats}, ${days}`;
3318
- }
3319
- var short = (outpoint) => {
3320
- const [txid = "", vout = ""] = outpoint.split(":");
3321
- return `${txid.slice(0, 8)}\u2026${txid.slice(-4)}:${vout}`;
3322
- };
3323
- var usable = (c) => c.sats >= MIN_SATS && c.age_blocks >= MIN_CONFIRMATIONS;
3324
- var dustText = (count) => `${n(count)} ${count === 1 ? "coin" : "coins"} under ${n(MIN_SATS)} sats, too small to back an account`;
3325
- var REFUSAL = {
3326
- invalid_address: "that isn't a Bitcoin address we can read",
3327
- chain_unavailable: "couldn't read the chain just now \xB7 nothing changed, try again in a minute",
3328
- sig_invalid: "that signature isn't from this coin's address \xB7 sign with the wallet that holds it",
3329
- bond_expired: "the message expired \xB7 start again",
3330
- bond_not_yet_valid: "your device's clock looks wrong \xB7 check it and start again",
3331
- bond_subject_mismatch: "that message was for another account \xB7 start again",
3332
- bond_malformed: "the signed message changed on the way \xB7 start again",
3333
- bond_window_too_long: "that message's signing window is too long \xB7 start again",
3334
- coin_not_found: "that coin doesn't exist on-chain",
3335
- coin_address_mismatch: "that coin isn't at this address",
3336
- coin_unconfirmed: "that coin isn't confirmed yet",
3337
- coin_too_recent: "that coin needs six confirmations first, about an hour",
3338
- coin_spent: "that coin has been spent \xB7 pick one still in your wallet",
3339
- coin_too_small: `that coin is under ${n(MIN_SATS)} sats, too small to back an account`,
3340
- coin_backs_another_account: "that coin already backs another orangecheck account",
3341
- coin_backed_another_recently: "that coin backed another account until recently \xB7 it can back this one 30 days after it was released",
3342
- not_authenticated: "your session ended \xB7 sign in again",
3343
- rate_limited: "too many tries \xB7 wait a minute, then try again"
3344
- };
3345
- var bondRefusal = (reason) => reason && REFUSAL[reason] || "your backing didn't save \xB7 nothing changed, try again";
3346
- var smallStyle = { ...bodyStyle, fontSize: 12, margin: 0 };
3347
- var explainerStyle = {
3348
- ...hintStyle,
3349
- fontSize: 12,
3350
- lineHeight: 1.55,
3351
- opacity: 1,
3352
- margin: 0
3353
- };
3354
- var fieldLabelStyle = {
3355
- display: "block",
3356
- color: V.foreground,
3357
- fontSize: 13,
3358
- fontWeight: 500,
3359
- margin: "0 0 4px"
3360
- };
3361
- var addressStyle = { wordBreak: "break-all", color: V.foreground };
3362
- function coinStyle(picked, young) {
3363
- return {
3364
- background: V.card,
3365
- border: `1px ${young ? "dashed" : "solid"} ${picked ? V.primary : V.border}`,
3366
- padding: "0.75rem",
3367
- display: "flex",
3368
- gap: 10,
3369
- alignItems: "flex-start",
3370
- cursor: young ? "not-allowed" : "pointer"
3371
- };
3372
- }
3373
- async function fetchOcBond(opts) {
3374
- const r = await fetch(`${opts?.authOrigin ?? DEFAULT_AUTH_ORIGIN2}/api/auth/bond`, {
3375
- credentials: "include"
3376
- });
3377
- const j = await r.json().catch(() => ({}));
3378
- if (!r.ok || !j.ok) throw new Error(`http_${r.status}`);
3379
- return j.bond ?? null;
3380
- }
3381
- function OcBond({
3382
- authOrigin = DEFAULT_AUTH_ORIGIN2,
3383
- onChange,
3384
- className
3385
- }) {
3386
- const [bond, setBond] = React4.useState(void 0);
3387
- const [loadErr, setLoadErr] = React4.useState(false);
3388
- const [nonce, setNonce] = React4.useState(0);
3389
- const [adding, setAdding] = React4.useState(false);
3390
- const [confirming, setConfirming] = React4.useState(false);
3391
- const [busy, setBusy] = React4.useState(false);
3392
- const [err, setErr] = React4.useState(null);
3393
- React4.useEffect(() => {
3394
- let alive = true;
3395
- fetchOcBond({ authOrigin }).then(
3396
- (b) => alive && setBond(b),
3397
- () => alive && setLoadErr(true)
3398
- );
3399
- return () => {
3400
- alive = false;
3401
- };
3402
- }, [authOrigin, nonce]);
3403
- const call = React4.useCallback(
3404
- async (method, body) => {
3405
- const r = await fetch(`${authOrigin}/api/auth/bond`, {
3406
- method,
3407
- credentials: "include",
3408
- headers: body ? { "Content-Type": "application/json" } : void 0,
3409
- body: body ? JSON.stringify(body) : void 0
3410
- });
3411
- const j = await r.json().catch(() => ({}));
3412
- const pin = readTabSession();
3413
- if (pin && r.ok && typeof j.token === "string") {
3414
- writeTabSession({ token: j.token, didOc: pin.didOc });
3415
- }
3416
- return { ok: r.ok && j.ok === true, ...j };
3417
- },
3418
- [authOrigin]
3419
- );
3420
- async function release() {
3421
- setBusy(true);
3422
- setErr(null);
3423
- const j = await call("DELETE").catch(() => ({ ok: false, reason: "network" }));
3424
- setBusy(false);
3425
- if (!j.ok) return setErr(bondRefusal(j.reason));
3426
- setConfirming(false);
3427
- setBond(null);
3428
- onChange?.(null);
3429
- }
3430
- if (loadErr) {
3431
- return /* @__PURE__ */ jsxs("div", { className, "data-oc-bond": "", style: panelStyle("warn"), role: "alert", children: [
3432
- /* @__PURE__ */ jsx("p", { style: bodyStyle, children: "couldn't read your backing \xB7 nothing was changed" }),
3433
- /* @__PURE__ */ jsx(
3434
- "button",
3435
- {
3436
- type: "button",
3437
- style: ghostBtnStyle(false),
3438
- onClick: () => {
3439
- setLoadErr(false);
3440
- setNonce((x) => x + 1);
3441
- },
3442
- children: "try again"
3443
- }
3444
- )
3445
- ] });
3446
- }
3447
- if (bond === void 0) {
3448
- return /* @__PURE__ */ jsx("div", { className, "data-oc-bond": "", style: panelStyle(), children: /* @__PURE__ */ jsx("span", { style: hintStyle, children: "reading your backing\u2026" }) });
3449
- }
3450
- return /* @__PURE__ */ jsxs("div", { className, "data-oc-bond": "", style: { display: "grid", gap: 16 }, children: [
3451
- bond && !adding ? /* @__PURE__ */ jsxs("div", { style: panelStyle("success"), children: [
3452
- /* @__PURE__ */ jsx(SectionLabel, { tone: "success", children: "\xA7 backed by bitcoin" }),
3453
- /* @__PURE__ */ jsxs("p", { style: { ...bodyStyle, color: V.foreground }, children: [
3454
- "apps see a coin ",
3455
- bondFloorsText(bond),
3456
- "."
3457
- ] }),
3458
- /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
3459
- "coin ",
3460
- short(bond.outpoint),
3461
- ", ",
3462
- n(bond.sats),
3463
- " sats \xB7 last found unspent at block ",
3464
- n(bond.checked_height),
3465
- " \xB7 only you and orangecheck see this line"
3466
- ] }),
3467
- confirming ? /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": "stop backing", "data-oc-bond-confirm": "", children: [
3468
- /* @__PURE__ */ jsx("p", { style: { ...bodyStyle, color: V.foreground, margin: 0 }, children: "stop backing? apps that ask for a backed account may stop letting you in." }),
3469
- /* @__PURE__ */ jsxs(FormButtons, { children: [
3470
- /* @__PURE__ */ jsx(
3471
- "button",
3472
- {
3473
- type: "button",
3474
- style: dangerBtnStyle(busy),
3475
- disabled: busy,
3476
- onClick: () => void release(),
3477
- children: "stop backing"
3478
- }
3479
- ),
3480
- /* @__PURE__ */ jsx(
3481
- "button",
3482
- {
3483
- type: "button",
3484
- style: ghostBtnStyle(busy),
3485
- disabled: busy,
3486
- autoFocus: true,
3487
- onClick: () => {
3488
- setErr(null);
3489
- setConfirming(false);
3490
- },
3491
- children: "keep"
3492
- }
3493
- )
3494
- ] })
3495
- ] }) : /* @__PURE__ */ jsxs(FormButtons, { children: [
3496
- /* @__PURE__ */ jsx(
3497
- "button",
3498
- {
3499
- type: "button",
3500
- style: ghostBtnStyle(false),
3501
- onClick: () => {
3502
- setErr(null);
3503
- setAdding(true);
3504
- },
3505
- children: "use another coin"
3506
- }
3507
- ),
3508
- /* @__PURE__ */ jsx(
3509
- "button",
3510
- {
3511
- type: "button",
3512
- style: dangerBtnStyle(false),
3513
- onClick: () => setConfirming(true),
3514
- children: "stop backing"
3515
- }
3516
- )
3517
- ] }),
3518
- err ? /* @__PURE__ */ jsx(ErrorLine, { children: err }) : null
3519
- ] }) : null,
3520
- !bond && !adding ? /* @__PURE__ */ jsxs("div", { style: panelStyle(), children: [
3521
- /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 not backed" }),
3522
- /* @__PURE__ */ jsx("p", { style: bodyStyle, children: "your account works everywhere as it is. some apps let in only accounts backed by a coin you hold. it stays in your wallet, and apps never see which coin." }),
3523
- /* @__PURE__ */ jsx(
3524
- "button",
3525
- {
3526
- type: "button",
3527
- style: primaryBtnStyle(false),
3528
- onClick: () => setAdding(true),
3529
- children: "back your account"
3530
- }
3531
- )
3532
- ] }) : null,
3533
- adding ? /* @__PURE__ */ jsx(
3534
- BondForm,
3535
- {
3536
- call,
3537
- onCancel: () => setAdding(false),
3538
- onDone: (b) => {
3539
- setBond(b);
3540
- setAdding(false);
3541
- onChange?.(b);
3542
- }
3543
- }
3544
- ) : null,
3545
- /* @__PURE__ */ jsx("p", { style: explainerStyle, children: "Apps see only rough floors, like \u201Cat least 100,000 sats, held 30 days or more\u201D, and a number unique to that app. Never the coin, your address or your balance. An app can block that number; only moving the coin gets past it, and a moved coin starts its age again." })
3546
- ] });
3547
- }
3548
- function BondForm({
3549
- call,
3550
- onCancel,
3551
- onDone
3552
- }) {
3553
- const [address, setAddress] = React4.useState("");
3554
- const [coins, setCoins] = React4.useState(null);
3555
- const [chosen, setChosen] = React4.useState("");
3556
- const [message, setMessage] = React4.useState(null);
3557
- const [busy, setBusy] = React4.useState(false);
3558
- const [err, setErr] = React4.useState(null);
3559
- const id = React4.useId();
3560
- const addr = address.trim();
3561
- const canFind = addr.length >= 14;
3562
- async function run(fn) {
3563
- setBusy(true);
3564
- setErr(null);
3565
- try {
3566
- await fn();
3567
- } catch {
3568
- setErr(bondRefusal("chain_unavailable"));
3569
- } finally {
3570
- setBusy(false);
3571
- }
3572
- }
3573
- const findCoins = () => run(async () => {
3574
- const j = await call("POST", { action: "coins", address: addr });
3575
- if (!j.ok) return setErr(bondRefusal(j.reason));
3576
- const list = j.coins ?? [];
3577
- setCoins(list);
3578
- setChosen(list.find(usable)?.outpoint ?? "");
3579
- });
3580
- const getMessage = () => run(async () => {
3581
- const j = await call("POST", { action: "message", address: addr, outpoint: chosen });
3582
- if (!j.ok || typeof j.message !== "string") return setErr(bondRefusal(j.reason));
3583
- setMessage(j.message);
3584
- });
3585
- const submit = (signature) => run(async () => {
3586
- const j = await call("POST", { action: "bond", message, signature });
3587
- if (!j.ok || !j.bond) return setErr(bondRefusal(j.reason));
3588
- onDone(j.bond);
3589
- });
3590
- function back() {
3591
- setErr(null);
3592
- if (message) setMessage(null);
3593
- else setCoins(null);
3594
- }
3595
- const error = err ? /* @__PURE__ */ jsx(ErrorLine, { children: err }) : null;
3596
- const backButton = /* @__PURE__ */ jsx("button", { type: "button", style: ghostBtnStyle(busy), disabled: busy, onClick: back, children: "back" });
3597
- const cancelButton = /* @__PURE__ */ jsx("button", { type: "button", style: ghostBtnStyle(busy), disabled: busy, onClick: onCancel, children: "cancel" });
3598
- let step;
3599
- if (message) {
3600
- step = /* @__PURE__ */ jsxs(Fragment, { children: [
3601
- /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
3602
- "sign with the wallet that holds ",
3603
- /* @__PURE__ */ jsx("span", { style: addressStyle, children: addr }),
3604
- ". signing spends nothing, and the coin stays in your wallet."
3605
- ] }),
3606
- /* @__PURE__ */ jsx(
3607
- LazyWalletButton,
3608
- {
3609
- address: addr,
3610
- message,
3611
- showManual: true,
3612
- onSigned: (sig) => void submit(sig),
3613
- onError: (e) => setErr(e.message ?? "the wallet refused to sign")
3614
- }
3615
- ),
3616
- error,
3617
- /* @__PURE__ */ jsxs(FormButtons, { children: [
3618
- backButton,
3619
- cancelButton
3620
- ] })
3621
- ] });
3622
- } else if (coins) {
3623
- const eligible = coins.filter((c) => c.sats >= MIN_SATS);
3624
- const dust = coins.length - eligible.length;
3625
- step = /* @__PURE__ */ jsxs(Fragment, { children: [
3626
- coins.length === 0 ? /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
3627
- "no confirmed coins at that address \xB7 send at least ",
3628
- n(MIN_SATS),
3629
- " sats there and wait for six confirmations, about an hour. the longer a coin sits, the more it says."
3630
- ] }) : eligible.length === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
3631
- /* @__PURE__ */ jsx("p", { style: { ...bodyStyle, color: V.foreground }, children: dustText(dust) }),
3632
- /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
3633
- "send at least ",
3634
- n(MIN_SATS),
3635
- " sats there in one payment and wait for six confirmations, about an hour. coins aren't added together."
3636
- ] })
3637
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3638
- /* @__PURE__ */ jsx("p", { id: `${id}-pick`, style: bodyStyle, children: "pick one coin \xB7 coins aren't added together" }),
3639
- /* @__PURE__ */ jsx(
3640
- "div",
3641
- {
3642
- role: "radiogroup",
3643
- "aria-labelledby": `${id}-pick`,
3644
- style: { display: "grid", gap: 8 },
3645
- children: eligible.map((c) => {
3646
- const young = c.age_blocks < MIN_CONFIRMATIONS;
3647
- return /* @__PURE__ */ jsxs(
3648
- "label",
3649
- {
3650
- style: coinStyle(chosen === c.outpoint, young),
3651
- children: [
3652
- /* @__PURE__ */ jsx(
3653
- "input",
3654
- {
3655
- type: "radio",
3656
- name: "oc-bond-coin",
3657
- value: c.outpoint,
3658
- checked: chosen === c.outpoint,
3659
- disabled: young,
3660
- onChange: () => setChosen(c.outpoint),
3661
- style: { accentColor: V.primary, margin: "2px 0 0" }
3662
- }
3663
- ),
3664
- /* @__PURE__ */ jsxs("span", { style: { display: "grid", gap: 2, minWidth: 0 }, children: [
3665
- /* @__PURE__ */ jsxs(
3666
- "span",
3667
- {
3668
- style: {
3669
- color: young ? V.muted : V.foreground,
3670
- fontSize: 13
3671
- },
3672
- children: [
3673
- n(c.sats),
3674
- " sats, held",
3675
- " ",
3676
- Math.floor(c.age_blocks / 144),
3677
- " days"
3678
- ]
3679
- }
3680
- ),
3681
- /* @__PURE__ */ jsx("span", { style: smallStyle, children: young ? "too recent: needs six confirmations" : `apps would see ${bondFloorsText(c)}` }),
3682
- /* @__PURE__ */ jsx(
3683
- "span",
3684
- {
3685
- style: {
3686
- ...smallStyle,
3687
- fontFamily: SIGNIN_FONT,
3688
- wordBreak: "break-all"
3689
- },
3690
- children: short(c.outpoint)
3691
- }
3692
- )
3693
- ] })
3694
- ]
3695
- },
3696
- c.outpoint
3697
- );
3698
- })
3699
- }
3700
- ),
3701
- dust > 0 ? /* @__PURE__ */ jsx("p", { style: { ...smallStyle, marginTop: 8 }, children: dustText(dust) }) : null
3702
- ] }),
3703
- error,
3704
- /* @__PURE__ */ jsxs(FormButtons, { children: [
3705
- eligible.length > 0 ? /* @__PURE__ */ jsx(
3706
- "button",
3707
- {
3708
- type: "button",
3709
- style: primaryBtnStyle(busy || !chosen),
3710
- disabled: busy || !chosen,
3711
- onClick: () => void getMessage(),
3712
- children: "continue"
3713
- }
3714
- ) : null,
3715
- backButton,
3716
- cancelButton
3717
- ] })
3718
- ] });
3719
- } else {
3720
- step = /* @__PURE__ */ jsxs(
3721
- "form",
3722
- {
3723
- style: { margin: 0 },
3724
- onSubmit: (e) => {
3725
- e.preventDefault();
3726
- if (canFind && !busy) void findCoins();
3727
- },
3728
- children: [
3729
- /* @__PURE__ */ jsx("label", { htmlFor: `${id}-address`, style: fieldLabelStyle, children: "bitcoin address that holds the coin" }),
3730
- /* @__PURE__ */ jsx("p", { id: `${id}-advice`, style: { ...bodyStyle, margin: "0 0 8px" }, children: "use one that holds nothing else of yours: orangecheck learns its address and amount. apps never do." }),
3731
- /* @__PURE__ */ jsx(
3732
- "input",
3733
- {
3734
- id: `${id}-address`,
3735
- "aria-describedby": `${id}-advice`,
3736
- style: inputStyle,
3737
- value: address,
3738
- onChange: (e) => setAddress(e.target.value),
3739
- placeholder: "bc1q\u2026",
3740
- spellCheck: false,
3741
- autoComplete: "off",
3742
- autoCapitalize: "off"
3743
- }
3744
- ),
3745
- error,
3746
- /* @__PURE__ */ jsxs(FormButtons, { children: [
3747
- /* @__PURE__ */ jsx(
3748
- "button",
3749
- {
3750
- type: "submit",
3751
- style: primaryBtnStyle(busy || !canFind),
3752
- disabled: busy || !canFind,
3753
- children: "find its coins"
3754
- }
3755
- ),
3756
- cancelButton
3757
- ] })
3758
- ]
3759
- }
3760
- );
3761
- }
3762
- return /* @__PURE__ */ jsxs("div", { style: panelStyle("accent"), "data-oc-bond-form": "", children: [
3763
- /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 back your account" }),
3764
- step
3765
- ] });
3766
- }
3767
- function resolveAuthOrigin(opts) {
3768
- if (!opts) return resolveConfig(void 0).authOrigin;
3769
- return resolveConfig(opts).authOrigin;
3770
- }
3771
- function isAbortError(err) {
3772
- if (!(err instanceof Error)) return false;
3773
- return /NotAllowed|aborted|cancel|user cancelled/i.test(err.message);
3774
- }
3775
- function useWebAuthnRegister(opts) {
3776
- const authOrigin = resolveAuthOrigin(opts);
3777
- const [status, setStatus] = React4.useState("idle");
3778
- const [error, setError] = React4.useState(null);
3779
- const reset = React4.useCallback(() => {
3780
- setStatus("idle");
3781
- setError(null);
3782
- }, []);
3783
- const register = React4.useCallback(
3784
- async (args) => {
3785
- setError(null);
3786
- setStatus("requesting-options");
3787
- try {
3788
- const optsRes = await fetch(`${authOrigin}/api/auth/webauthn/register/options`, {
3789
- method: "POST",
3790
- credentials: "include",
3791
- headers: { "Content-Type": "application/json" },
3792
- body: JSON.stringify({ label: args?.label })
3793
- });
3794
- const optsBody = await optsRes.json();
3795
- if (!optsRes.ok || !optsBody.ok || !optsBody.options || !optsBody.challenge_token) {
3796
- const reason = optsBody.reason ?? `options_failed_${optsRes.status}`;
3797
- setError(new Error(reason));
3798
- setStatus("error");
3799
- return { ok: false, reason };
3800
- }
3801
- setStatus("authenticating");
3802
- let attestation;
3803
- try {
3804
- attestation = await startRegistration({ optionsJSON: optsBody.options });
3805
- } catch (e) {
3806
- const reason = isAbortError(e) ? "cancelled" : e.message;
3807
- setError(new Error(reason));
3808
- setStatus("error");
3809
- return { ok: false, reason };
3810
- }
3811
- setStatus("verifying");
3812
- const verifyRes = await fetch(`${authOrigin}/api/auth/webauthn/register/verify`, {
3813
- method: "POST",
3814
- credentials: "include",
3815
- headers: { "Content-Type": "application/json" },
3816
- body: JSON.stringify({
3817
- challenge_token: optsBody.challenge_token,
3818
- response: attestation,
3819
- label: args?.label
3820
- })
3821
- });
3822
- const verifyBody = await verifyRes.json();
3823
- if (!verifyRes.ok || !verifyBody.ok || !verifyBody.credential) {
3824
- const reason = verifyBody.reason ?? `verify_failed_${verifyRes.status}`;
3825
- setError(new Error(reason));
3826
- setStatus("error");
3827
- return { ok: false, reason };
3828
- }
3829
- setStatus("success");
3830
- return { ok: true, credential: verifyBody.credential };
3831
- } catch (e) {
3832
- const reason = e instanceof Error ? e.message : String(e);
3833
- setError(new Error(reason));
3834
- setStatus("error");
3835
- return { ok: false, reason };
3836
- }
3837
- },
3838
- [authOrigin]
3839
- );
3840
- return { status, error, register, reset };
3841
- }
3842
- function useWebAuthnList(opts) {
3843
- const authOrigin = resolveAuthOrigin(opts);
3844
- const { status: sessionStatus } = useOcSession();
3845
- const [credentials, setCredentials] = React4.useState([]);
3846
- const [status, setStatus] = React4.useState("loading");
3847
- const [error, setError] = React4.useState(null);
3848
- const refetch = React4.useCallback(async () => {
3849
- setError(null);
3850
- if (sessionStatus !== "authenticated") {
3851
- setCredentials([]);
3852
- setStatus("ready");
3853
- return;
3854
- }
3855
- setStatus("loading");
3856
- try {
3857
- const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials`, {
3858
- credentials: "include"
3859
- });
3860
- if (r.status === 401) {
3861
- setCredentials([]);
3862
- setStatus("ready");
3863
- return;
3864
- }
3865
- const j = await r.json();
3866
- if (!j.ok || !j.credentials) {
3867
- throw new Error(j.reason ?? "list_failed");
3868
- }
3869
- setCredentials(j.credentials);
3870
- setStatus("ready");
3871
- } catch (e) {
3872
- setError(e instanceof Error ? e : new Error(String(e)));
3873
- setStatus("error");
3874
- }
3875
- }, [authOrigin, sessionStatus]);
3876
- React4.useEffect(() => {
3877
- void refetch();
3878
- }, [refetch]);
3879
- const rename = React4.useCallback(
3880
- async (id, label) => {
3881
- try {
3882
- const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
3883
- method: "PATCH",
3884
- credentials: "include",
3885
- headers: { "Content-Type": "application/json" },
3886
- body: JSON.stringify({ label })
3887
- });
3888
- const j = await r.json();
3889
- if (!j.ok || !j.credential) {
3890
- const reason = j.reason ?? `rename_failed_${r.status}`;
3891
- setError(new Error(reason));
3892
- return { ok: false, reason };
3893
- }
3894
- await refetch();
3895
- return { ok: true, credential: j.credential };
3896
- } catch (e) {
3897
- const reason = e instanceof Error ? e.message : String(e);
3898
- setError(new Error(reason));
3899
- return { ok: false, reason };
3900
- }
3901
- },
3902
- [authOrigin, refetch]
3903
- );
3904
- const remove = React4.useCallback(
3905
- async (id) => {
3906
- try {
3907
- const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
3908
- method: "DELETE",
3909
- credentials: "include"
3910
- });
3911
- const j = await r.json();
3912
- if (!j.ok) {
3913
- const reason = j.reason ?? `delete_failed_${r.status}`;
3914
- setError(new Error(reason));
3915
- return { ok: false, reason };
3916
- }
3917
- await refetch();
3918
- return { ok: true };
3919
- } catch (e) {
3920
- const reason = e instanceof Error ? e.message : String(e);
3921
- setError(new Error(reason));
3922
- return { ok: false, reason };
3923
- }
3924
- },
3925
- [authOrigin, refetch]
3926
- );
3927
- return { status, credentials, error, refetch, rename, remove };
3928
- }
3929
- function useStepUpAuth(opts) {
3930
- const authOrigin = resolveAuthOrigin(opts);
3931
- const { refresh } = useOcSession();
3932
- const [status, setStatus] = React4.useState("idle");
3933
- const [error, setError] = React4.useState(null);
3934
- const reset = React4.useCallback(() => {
3935
- setStatus("idle");
3936
- setError(null);
3937
- }, []);
3938
- const stepUp = React4.useCallback(
3939
- async (args) => {
3940
- setError(null);
3941
- setStatus("requesting-options");
3942
- try {
3943
- const optsRes = await fetch(`${authOrigin}/api/auth/webauthn/assertion/options`, {
3944
- method: "POST",
3945
- credentials: "include",
3946
- headers: { "Content-Type": "application/json" },
3947
- body: JSON.stringify({ purpose: args.purpose })
3948
- });
3949
- const optsBody = await optsRes.json();
3950
- if (!optsRes.ok || !optsBody.ok || !optsBody.options || !optsBody.challenge_token) {
3951
- const reason = optsBody.reason ?? `options_failed_${optsRes.status}`;
3952
- setError(new Error(reason));
3953
- setStatus("error");
3954
- return { ok: false, reason };
3955
- }
3956
- setStatus("authenticating");
3957
- let assertion;
3958
- try {
3959
- assertion = await startAuthentication({ optionsJSON: optsBody.options });
3960
- } catch (e) {
3961
- const reason = isAbortError(e) ? "cancelled" : e.message;
3962
- setError(new Error(reason));
3963
- setStatus("error");
3964
- return { ok: false, reason };
3965
- }
3966
- setStatus("verifying");
3967
- const verifyRes = await fetch(
3968
- `${authOrigin}/api/auth/webauthn/assertion/verify`,
3969
- {
3970
- method: "POST",
3971
- credentials: "include",
3972
- headers: { "Content-Type": "application/json" },
3973
- body: JSON.stringify({
3974
- challenge_token: optsBody.challenge_token,
3975
- response: assertion
3976
- })
3977
- }
3978
- );
3979
- const verifyBody = await verifyRes.json();
3980
- if (!verifyRes.ok || !verifyBody.ok || typeof verifyBody.step_up_at !== "number") {
3981
- const reason = verifyBody.reason ?? `verify_failed_${verifyRes.status}`;
3982
- setError(new Error(reason));
3983
- setStatus("error");
3984
- return { ok: false, reason };
3985
- }
3986
- await refresh();
3987
- setStatus("success");
3988
- return { ok: true, step_up_at: verifyBody.step_up_at };
3989
- } catch (e) {
3990
- const reason = e instanceof Error ? e.message : String(e);
3991
- setError(new Error(reason));
3992
- setStatus("error");
3993
- return { ok: false, reason };
3994
- }
3995
- },
3996
- [authOrigin, refresh]
3997
- );
3998
- return { status, error, stepUp, reset };
3999
- }
4000
-
4001
- export { DEFAULT_CONFIG, DISPLAY_IDENTITY_KINDS, FRAGMENT_TTL_MS, OcAccountChip, OcAccountPill, OcAddressInput, OcBond, OcLinkedIdentities, OcSessionProvider, OcSignIn, OcSignInButton, SUDO_ACCOUNT_MISMATCH_MESSAGE, TAB_ACCOUNT_HINT_KEY, TAB_ADOPT_HASH, TAB_SESSION_HEADER, TAB_SESSION_STORAGE_KEY, bondFloorsText, buildAddAccountUrl, buildSignInUrl, clearTabSession, consumeTabAccountHint, consumeTabAdoptMarker, familyReturnTarget, fetchOcBond, fetchOcLinkedIdentities, handleSudoRequired, installTabFetchInterceptor, installTabLinkDecorator, isSudoAccountMismatch, keepFragmentOnReturnLinks, readTabSession, redirectToSudo, restoreReturnFragment, returnToHere, safeReturnTo, stripFragment, tabSessionHeader, useOcAddressSuggestion, useOcSession, useOptionalOcSession, useStepUpAuth, useWebAuthnList, useWebAuthnRegister, withoutFragment, writeTabSession };
4002
- //# sourceMappingURL=index.mjs.map
4003
114
  //# sourceMappingURL=index.mjs.map