@orangecheck/auth-client 2.33.4 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/appearance.mjs +23 -0
  2. package/dist/appearance.mjs.map +1 -0
  3. package/dist/bond.mjs +20 -0
  4. package/dist/bond.mjs.map +1 -0
  5. package/dist/chunk-4E7GJ5IB.mjs +1184 -0
  6. package/dist/chunk-4E7GJ5IB.mjs.map +1 -0
  7. package/dist/chunk-7ST4Q5KR.mjs +346 -0
  8. package/dist/chunk-7ST4Q5KR.mjs.map +1 -0
  9. package/dist/chunk-7UYPMTAV.mjs +652 -0
  10. package/dist/chunk-7UYPMTAV.mjs.map +1 -0
  11. package/dist/chunk-BYAF2P2L.mjs +253 -0
  12. package/dist/chunk-BYAF2P2L.mjs.map +1 -0
  13. package/dist/chunk-CYR7J4UY.mjs +118 -0
  14. package/dist/chunk-CYR7J4UY.mjs.map +1 -0
  15. package/dist/chunk-H2TXOCXH.mjs +23 -0
  16. package/dist/chunk-H2TXOCXH.mjs.map +1 -0
  17. package/dist/chunk-HZ4FJMF3.mjs +490 -0
  18. package/dist/chunk-HZ4FJMF3.mjs.map +1 -0
  19. package/dist/chunk-TJJNTITX.mjs +187 -0
  20. package/dist/chunk-TJJNTITX.mjs.map +1 -0
  21. package/dist/chunk-UFRU3I3D.mjs +913 -0
  22. package/dist/chunk-UFRU3I3D.mjs.map +1 -0
  23. package/dist/chunk-VRLFZAGK.mjs +37 -0
  24. package/dist/chunk-VRLFZAGK.mjs.map +1 -0
  25. package/dist/chunk-XCZVSE7M.mjs +40 -0
  26. package/dist/chunk-XCZVSE7M.mjs.map +1 -0
  27. package/dist/components.mjs +19 -0
  28. package/dist/components.mjs.map +1 -0
  29. package/dist/index.js +77 -67
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +112 -4001
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/linked-identities.mjs +41 -0
  34. package/dist/linked-identities.mjs.map +1 -0
  35. package/dist/provider.mjs +17 -0
  36. package/dist/provider.mjs.map +1 -0
  37. package/dist/return-to.mjs +18 -0
  38. package/dist/return-to.mjs.map +1 -0
  39. package/dist/signin.mjs +23 -0
  40. package/dist/signin.mjs.map +1 -0
  41. package/dist/sudo.mjs +16 -0
  42. package/dist/sudo.mjs.map +1 -0
  43. package/dist/tab-session.mjs +29 -0
  44. package/dist/tab-session.mjs.map +1 -0
  45. package/dist/types.mjs +17 -0
  46. package/dist/types.mjs.map +1 -0
  47. package/dist/webauthn.mjs +15 -0
  48. package/dist/webauthn.mjs.map +1 -0
  49. package/package.json +2 -1
  50. package/dist/index.d.mts +0 -303
@@ -0,0 +1,913 @@
1
+ import {
2
+ SUDO_ACCOUNT_MISMATCH_MESSAGE,
3
+ handleSudoRequired,
4
+ isSudoAccountMismatch
5
+ } from "./chunk-XCZVSE7M.mjs";
6
+ import {
7
+ SIGNIN_BUTTON_RADIUS,
8
+ SIGNIN_CASE,
9
+ SIGNIN_FONT,
10
+ signinRadius,
11
+ signinSize,
12
+ signinTracking
13
+ } from "./chunk-H2TXOCXH.mjs";
14
+ import {
15
+ tabSessionHeader
16
+ } from "./chunk-TJJNTITX.mjs";
17
+
18
+ // src/linked-identities.tsx
19
+ import * as React from "react";
20
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
21
+ var DEFAULT_AUTH_ORIGIN = "https://ochk.io";
22
+ async function fetchOcLinkedIdentities(opts) {
23
+ const authOrigin = opts?.authOrigin ?? DEFAULT_AUTH_ORIGIN;
24
+ const r = await fetch(`${authOrigin}/api/auth/identities`, {
25
+ credentials: "include",
26
+ headers: { ...tabSessionHeader() }
27
+ });
28
+ if (r.status === 401) return [];
29
+ if (!r.ok) throw new Error(`identities fetch failed: http_${r.status}`);
30
+ const body = await r.json();
31
+ return body.linked ?? [];
32
+ }
33
+ function OcLinkedIdentities({
34
+ authOrigin = DEFAULT_AUTH_ORIGIN,
35
+ className,
36
+ onChange,
37
+ attestationLink = true
38
+ }) {
39
+ const [resp, setResp] = React.useState(null);
40
+ const [error, setError] = React.useState(null);
41
+ const [showEmailLink, setShowEmailLink] = React.useState(false);
42
+ const [showBtcLink, setShowBtcLink] = React.useState(false);
43
+ const refresh = React.useCallback(async () => {
44
+ setError(null);
45
+ try {
46
+ const r = await fetch(`${authOrigin}/api/auth/identities`, {
47
+ credentials: "include",
48
+ headers: { ...tabSessionHeader() }
49
+ });
50
+ if (!r.ok) {
51
+ if (r.status === 401) {
52
+ setError("not signed in");
53
+ return;
54
+ }
55
+ throw new Error(`http_${r.status}`);
56
+ }
57
+ setResp(await r.json());
58
+ } catch (e) {
59
+ setError(e instanceof Error ? e.message : "load failed");
60
+ }
61
+ }, [authOrigin]);
62
+ React.useEffect(() => {
63
+ void refresh();
64
+ }, [refresh]);
65
+ const afterChange = React.useCallback(() => {
66
+ setShowEmailLink(false);
67
+ setShowBtcLink(false);
68
+ void refresh();
69
+ onChange?.();
70
+ }, [refresh, onChange]);
71
+ if (error) {
72
+ return /* @__PURE__ */ jsxs("div", { className, "data-oc-li": "", style: panelStyle("warn"), role: "alert", children: [
73
+ /* @__PURE__ */ jsx(SectionLabel, { tone: "warn", children: "\xA7 identities \xB7 load deferred" }),
74
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
75
+ "Couldn't reach the auth host (",
76
+ error,
77
+ "). Linking is temporarily unavailable; your signed-in identity still works as normal."
78
+ ] }),
79
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void refresh(), style: ghostBtnStyle(false), children: "retry" })
80
+ ] });
81
+ }
82
+ if (!resp) {
83
+ return /* @__PURE__ */ jsx("div", { className, "data-oc-li": "", style: panelStyle(), children: /* @__PURE__ */ jsx("span", { style: hintStyle, children: "loading\u2026" }) });
84
+ }
85
+ const linked = resp.linked ?? [];
86
+ const emailRows = linked.filter((i) => i.kind === "email");
87
+ const btcRows = linked.filter((i) => i.kind === "btc");
88
+ const hasEmail = emailRows.some((i) => i.value);
89
+ const hasBtc = btcRows.some((i) => i.value);
90
+ const mergedFrom = resp.merged_from ?? [];
91
+ return /* @__PURE__ */ jsxs("div", { className, "data-oc-li": "", style: { display: "grid", gap: 16 }, children: [
92
+ /* @__PURE__ */ jsxs("div", { style: panelStyle(), children: [
93
+ /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 what this is" }),
94
+ /* @__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." })
95
+ ] }),
96
+ mergedFrom.length > 0 && /* @__PURE__ */ jsx(ConsolidationPanel, { entries: mergedFrom }),
97
+ /* @__PURE__ */ jsxs("div", { style: panelStyle("plain"), "data-oc-li-rows": "", children: [
98
+ /* @__PURE__ */ jsx(
99
+ IdentityGroup,
100
+ {
101
+ kind: "email",
102
+ title: "email",
103
+ rows: emailRows,
104
+ authOrigin,
105
+ onChange: afterChange,
106
+ addCta: hasEmail ? null : "link an email",
107
+ onAdd: () => {
108
+ setShowEmailLink((v) => !v);
109
+ setShowBtcLink(false);
110
+ }
111
+ }
112
+ ),
113
+ /* @__PURE__ */ jsx("div", { style: { borderTop: `1px solid ${V.border}` } }),
114
+ /* @__PURE__ */ jsx(
115
+ IdentityGroup,
116
+ {
117
+ kind: "btc",
118
+ title: "bitcoin address",
119
+ rows: btcRows,
120
+ authOrigin,
121
+ onChange: afterChange,
122
+ addCta: hasBtc ? null : "link a Bitcoin address",
123
+ onAdd: () => {
124
+ setShowBtcLink((v) => !v);
125
+ setShowEmailLink(false);
126
+ }
127
+ }
128
+ )
129
+ ] }),
130
+ showEmailLink && /* @__PURE__ */ jsx(
131
+ EmailLinkForm,
132
+ {
133
+ authOrigin,
134
+ onDone: afterChange,
135
+ onCancel: () => setShowEmailLink(false)
136
+ }
137
+ ),
138
+ showBtcLink && /* @__PURE__ */ jsx(
139
+ BtcLinkForm,
140
+ {
141
+ authOrigin,
142
+ didOc: resp.did_oc ?? null,
143
+ onDone: afterChange,
144
+ onCancel: () => setShowBtcLink(false)
145
+ }
146
+ ),
147
+ hasBtc && attestationLink && /* @__PURE__ */ jsx(AttestationLink, {})
148
+ ] });
149
+ }
150
+ function IdentityGroup({
151
+ kind,
152
+ title,
153
+ rows,
154
+ authOrigin,
155
+ onChange,
156
+ addCta,
157
+ onAdd
158
+ }) {
159
+ if (rows.length === 0) {
160
+ return /* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
161
+ /* @__PURE__ */ jsxs("div", { children: [
162
+ /* @__PURE__ */ jsxs("div", { style: rowLabelStyle, children: [
163
+ "\xA7 ",
164
+ title
165
+ ] }),
166
+ /* @__PURE__ */ jsx("div", { style: { ...bodyStyle, marginTop: 4 }, children: "none linked" })
167
+ ] }),
168
+ addCta && /* @__PURE__ */ jsxs("button", { type: "button", onClick: onAdd, style: primaryChipStyle, children: [
169
+ "+ ",
170
+ addCta
171
+ ] })
172
+ ] });
173
+ }
174
+ return /* @__PURE__ */ jsx(Fragment, { children: rows.map((r) => /* @__PURE__ */ jsx(
175
+ IdentityRow,
176
+ {
177
+ row: r,
178
+ kind,
179
+ authOrigin,
180
+ onChange
181
+ },
182
+ r.id
183
+ )) });
184
+ }
185
+ function IdentityRow({
186
+ row,
187
+ kind,
188
+ authOrigin,
189
+ onChange
190
+ }) {
191
+ const [shown, setShown] = React.useState(false);
192
+ const [confirming, setConfirming] = React.useState(false);
193
+ const [busy, setBusy] = React.useState(false);
194
+ const [err, setErr] = React.useState(null);
195
+ const masked = row.value ? mask(row.value, kind) : "\u2014 pending recovery \u2014";
196
+ const display = shown && row.value ? row.value : masked;
197
+ async function unlink() {
198
+ if (row.is_primary) return;
199
+ setBusy(true);
200
+ setErr(null);
201
+ try {
202
+ const r = await fetch(
203
+ `${authOrigin}/api/auth/identities?id=${encodeURIComponent(row.id)}`,
204
+ { method: "DELETE", credentials: "include" }
205
+ );
206
+ if (!r.ok) {
207
+ const j = await r.json().catch(() => ({}));
208
+ throw new Error(j.reason ?? `http_${r.status}`);
209
+ }
210
+ onChange();
211
+ } catch (e) {
212
+ setErr(e instanceof Error ? e.message : "unlink failed");
213
+ setBusy(false);
214
+ setConfirming(false);
215
+ }
216
+ }
217
+ return /* @__PURE__ */ jsxs("div", { style: rowStyle, "data-oc-li-row": kind, children: [
218
+ /* @__PURE__ */ jsxs("div", { style: { minWidth: 0, flex: 1 }, children: [
219
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: "4px 12px" }, children: [
220
+ /* @__PURE__ */ jsxs("span", { style: rowLabelStyle, children: [
221
+ "\xA7 ",
222
+ kind === "email" ? "email" : "bitcoin address"
223
+ ] }),
224
+ row.is_primary && /* @__PURE__ */ jsx("span", { style: { ...rowLabelStyle, color: V.primary }, children: "primary \xB7 signup-time" })
225
+ ] }),
226
+ /* @__PURE__ */ jsx(
227
+ "div",
228
+ {
229
+ style: {
230
+ marginTop: 4,
231
+ color: V.foreground,
232
+ fontFamily: SIGNIN_FONT,
233
+ fontSize: signinSize(12),
234
+ wordBreak: "break-all"
235
+ },
236
+ children: display
237
+ }
238
+ ),
239
+ row.verified_at && /* @__PURE__ */ jsxs("div", { style: { ...hintStyle, marginTop: 4 }, children: [
240
+ "verified \xB7 ",
241
+ new Date(row.verified_at).toUTCString()
242
+ ] }),
243
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err })
244
+ ] }),
245
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexShrink: 0, gap: 8, flexWrap: "wrap" }, children: [
246
+ row.value && /* @__PURE__ */ jsx(
247
+ "button",
248
+ {
249
+ type: "button",
250
+ onClick: () => setShown((s) => !s),
251
+ style: ghostBtnStyle(false),
252
+ children: shown ? "hide" : "show"
253
+ }
254
+ ),
255
+ !row.is_primary && !confirming && /* @__PURE__ */ jsx(
256
+ "button",
257
+ {
258
+ type: "button",
259
+ onClick: () => setConfirming(true),
260
+ style: dangerBtnStyle(false),
261
+ children: "unlink"
262
+ }
263
+ ),
264
+ !row.is_primary && confirming && /* @__PURE__ */ jsxs(Fragment, { children: [
265
+ /* @__PURE__ */ jsx(
266
+ "button",
267
+ {
268
+ type: "button",
269
+ onClick: () => void unlink(),
270
+ disabled: busy,
271
+ style: dangerBtnStyle(busy),
272
+ children: busy ? "unlinking\u2026" : "confirm unlink"
273
+ }
274
+ ),
275
+ /* @__PURE__ */ jsx(
276
+ "button",
277
+ {
278
+ type: "button",
279
+ onClick: () => setConfirming(false),
280
+ disabled: busy,
281
+ style: ghostBtnStyle(busy),
282
+ children: "cancel"
283
+ }
284
+ )
285
+ ] })
286
+ ] })
287
+ ] });
288
+ }
289
+ function ConsolidationPanel({ entries }) {
290
+ return /* @__PURE__ */ jsxs("div", { style: panelStyle("success"), "data-oc-li-consolidated": "", children: [
291
+ /* @__PURE__ */ jsxs(SectionLabel, { tone: "success", children: [
292
+ "\xA7 consolidated accounts \xB7 ",
293
+ entries.length
294
+ ] }),
295
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
296
+ entries.length === 1 ? "A previous OC account" : "Previous OC accounts",
297
+ " you signed up with, folded into this one under dual proof of control. Historical activity is unioned into this account automatically;",
298
+ " ",
299
+ entries.length === 1 ? "it" : "they",
300
+ " can no longer sign in."
301
+ ] }),
302
+ /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", margin: "12px 0 0", padding: 0 }, children: entries.map((e) => /* @__PURE__ */ jsxs(
303
+ "li",
304
+ {
305
+ style: {
306
+ display: "flex",
307
+ flexWrap: "wrap",
308
+ gap: 8,
309
+ padding: "8px 0",
310
+ borderTop: `1px solid ${V.border}`,
311
+ fontFamily: SIGNIN_FONT,
312
+ fontSize: signinSize(11)
313
+ },
314
+ children: [
315
+ /* @__PURE__ */ jsx("code", { style: { color: V.foreground, wordBreak: "break-all" }, children: e.did_oc }),
316
+ /* @__PURE__ */ jsxs("span", { style: { ...hintStyle, marginLeft: "auto" }, children: [
317
+ "created ",
318
+ new Date(e.created_at).toLocaleDateString()
319
+ ] })
320
+ ]
321
+ },
322
+ e.did_oc
323
+ )) })
324
+ ] });
325
+ }
326
+ function AttestationLink() {
327
+ return /* @__PURE__ */ jsxs("div", { style: panelStyle(), "data-oc-li-attest": "", children: [
328
+ /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 portable attestation" }),
329
+ /* @__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." }),
330
+ /* @__PURE__ */ jsx(
331
+ "a",
332
+ {
333
+ href: "https://attest.ochk.io/create",
334
+ target: "_blank",
335
+ rel: "noreferrer",
336
+ style: { ...primaryChipStyle, textDecoration: "none", display: "inline-block" },
337
+ "data-oc-li-attest-link": "",
338
+ children: "open the attestation builder \u2197"
339
+ }
340
+ )
341
+ ] });
342
+ }
343
+ var ACCOUNT_WAYS_URL = "https://ochk.app/me/account#ways";
344
+ function LinkPromptStep({
345
+ method,
346
+ didOc,
347
+ authOrigin,
348
+ onResolved
349
+ }) {
350
+ const what = method === "btc" ? "a Bitcoin address" : "an email";
351
+ return /* @__PURE__ */ jsxs("div", { "data-oc-linkprompt": method, children: [
352
+ /* @__PURE__ */ jsxs("div", { style: { ...panelStyle("success"), marginBottom: 16 }, children: [
353
+ /* @__PURE__ */ jsx(SectionLabel, { tone: "success", children: "\xA7 signed in" }),
354
+ /* @__PURE__ */ jsxs("p", { style: { ...bodyStyle, margin: 0 }, children: [
355
+ "You chose to also link ",
356
+ what,
357
+ ". Complete it below \u2014 or skip for now; it is entirely optional and changes nothing about the sign-in you just completed."
358
+ ] })
359
+ ] }),
360
+ method === "btc" ? /* @__PURE__ */ jsx(
361
+ BtcLinkForm,
362
+ {
363
+ authOrigin,
364
+ didOc,
365
+ onDone: onResolved,
366
+ onCancel: onResolved
367
+ }
368
+ ) : /* @__PURE__ */ jsx(EmailLinkForm, { authOrigin, onDone: onResolved, onCancel: onResolved }),
369
+ /* @__PURE__ */ jsxs(
370
+ "div",
371
+ {
372
+ "data-oc-linkprompt-skip": "",
373
+ style: { marginTop: 14, paddingTop: 14, borderTop: `1px solid ${V.border}` },
374
+ children: [
375
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onResolved, style: ghostBtnStyle(false), children: "skip \u2014 I'll do this later" }),
376
+ /* @__PURE__ */ jsxs("p", { style: { ...hintStyle, marginTop: 8 }, children: [
377
+ "Changed your mind? Skipping continues you straight on \u2014 your sign-in is unaffected. You can link ",
378
+ what,
379
+ " anytime later from your account page,",
380
+ " ",
381
+ /* @__PURE__ */ jsx(
382
+ "a",
383
+ {
384
+ href: ACCOUNT_WAYS_URL,
385
+ target: "_blank",
386
+ rel: "noreferrer",
387
+ style: { color: "inherit" },
388
+ "data-oc-linkprompt-account": "",
389
+ children: "ochk.app/me/account"
390
+ }
391
+ ),
392
+ "."
393
+ ] })
394
+ ]
395
+ }
396
+ )
397
+ ] });
398
+ }
399
+ function EmailLinkForm({
400
+ authOrigin,
401
+ onDone,
402
+ onCancel
403
+ }) {
404
+ const [step, setStep] = React.useState("email");
405
+ const [email, setEmail] = React.useState("");
406
+ const [code, setCode] = React.useState("");
407
+ const [token, setToken] = React.useState("");
408
+ const [busy, setBusy] = React.useState(false);
409
+ const [err, setErr] = React.useState(null);
410
+ const [transfer, setTransfer] = React.useState(false);
411
+ async function start() {
412
+ setBusy(true);
413
+ setErr(null);
414
+ try {
415
+ const r = await fetch(`${authOrigin}/api/auth/email-otp/start`, {
416
+ method: "POST",
417
+ credentials: "include",
418
+ headers: { "Content-Type": "application/json" },
419
+ body: JSON.stringify({ email: email.trim().toLowerCase() })
420
+ });
421
+ const j = await r.json();
422
+ if (!r.ok || !j.token) throw new Error(j.reason ?? `http_${r.status}`);
423
+ setToken(j.token);
424
+ setStep("code");
425
+ } catch (e) {
426
+ setErr(e instanceof Error ? e.message : "could not send code");
427
+ } finally {
428
+ setBusy(false);
429
+ }
430
+ }
431
+ async function verify(confirmTransfer) {
432
+ setBusy(true);
433
+ setErr(null);
434
+ try {
435
+ const r = await fetch(`${authOrigin}/api/auth/link/email`, {
436
+ method: "POST",
437
+ credentials: "include",
438
+ headers: { "Content-Type": "application/json" },
439
+ body: JSON.stringify({
440
+ email: email.trim().toLowerCase(),
441
+ code: code.trim(),
442
+ token,
443
+ ...confirmTransfer ? { confirm_transfer: true } : {}
444
+ })
445
+ });
446
+ const j = await r.json();
447
+ if (r.status === 409 && j.reason === "email_linked_elsewhere") {
448
+ setTransfer(true);
449
+ setBusy(false);
450
+ return;
451
+ }
452
+ if (handleSudoRequired(j, { purpose: "link an email" })) return;
453
+ if (isSudoAccountMismatch(j)) {
454
+ setErr(SUDO_ACCOUNT_MISMATCH_MESSAGE);
455
+ setBusy(false);
456
+ return;
457
+ }
458
+ if (!r.ok || !j.ok) throw new Error(j.reason ?? `http_${r.status}`);
459
+ onDone();
460
+ } catch (e) {
461
+ setErr(e instanceof Error ? e.message : "link failed");
462
+ setBusy(false);
463
+ }
464
+ }
465
+ return /* @__PURE__ */ jsxs("div", { style: panelStyle("accent"), "data-oc-li-form": "email", children: [
466
+ /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 link an email" }),
467
+ step === "email" ? /* @__PURE__ */ jsxs(Fragment, { children: [
468
+ /* @__PURE__ */ jsx(
469
+ "input",
470
+ {
471
+ type: "email",
472
+ autoComplete: "email",
473
+ value: email,
474
+ onChange: (e) => setEmail(e.target.value),
475
+ placeholder: "you@example.com",
476
+ style: inputStyle
477
+ }
478
+ ),
479
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
480
+ /* @__PURE__ */ jsxs(FormButtons, { children: [
481
+ /* @__PURE__ */ jsx(
482
+ "button",
483
+ {
484
+ type: "button",
485
+ onClick: () => void start(),
486
+ disabled: busy || email.trim().length < 5,
487
+ style: primaryBtnStyle(busy || email.trim().length < 5),
488
+ children: busy ? "sending\u2026" : "send code"
489
+ }
490
+ ),
491
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, style: ghostBtnStyle(busy), children: "cancel" })
492
+ ] })
493
+ ] }) : transfer ? /* @__PURE__ */ jsx(
494
+ TransferConfirm,
495
+ {
496
+ what: "email",
497
+ busy,
498
+ onConfirm: () => void verify(true),
499
+ onCancel,
500
+ err
501
+ }
502
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
503
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
504
+ "Enter the 6-digit code sent to ",
505
+ email,
506
+ "."
507
+ ] }),
508
+ /* @__PURE__ */ jsx(
509
+ "input",
510
+ {
511
+ type: "text",
512
+ inputMode: "numeric",
513
+ autoComplete: "one-time-code",
514
+ maxLength: 6,
515
+ value: code,
516
+ onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
517
+ placeholder: "123456",
518
+ style: { ...inputStyle, letterSpacing: "0.3em" }
519
+ }
520
+ ),
521
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
522
+ /* @__PURE__ */ jsxs(FormButtons, { children: [
523
+ /* @__PURE__ */ jsx(
524
+ "button",
525
+ {
526
+ type: "button",
527
+ onClick: () => void verify(false),
528
+ disabled: busy || code.length !== 6,
529
+ style: primaryBtnStyle(busy || code.length !== 6),
530
+ children: busy ? "linking\u2026" : "verify + link"
531
+ }
532
+ ),
533
+ /* @__PURE__ */ jsx(
534
+ "button",
535
+ {
536
+ type: "button",
537
+ onClick: () => setStep("email"),
538
+ disabled: busy,
539
+ style: ghostBtnStyle(busy),
540
+ children: "back"
541
+ }
542
+ )
543
+ ] })
544
+ ] })
545
+ ] });
546
+ }
547
+ function BtcLinkForm({
548
+ authOrigin,
549
+ didOc,
550
+ onDone,
551
+ onCancel
552
+ }) {
553
+ const [step, setStep] = React.useState("request");
554
+ const [addr, setAddr] = React.useState("");
555
+ const [challenge, setChallenge] = React.useState("");
556
+ const [busy, setBusy] = React.useState(false);
557
+ const [err, setErr] = React.useState(null);
558
+ const [pendingSig, setPendingSig] = React.useState(null);
559
+ async function getChallenge() {
560
+ if (!didOc) {
561
+ setErr("session not loaded yet \xB7 refresh and retry");
562
+ return;
563
+ }
564
+ setBusy(true);
565
+ setErr(null);
566
+ try {
567
+ const r = await fetch(
568
+ `${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&purpose=link&audience=${encodeURIComponent(didOc)}`
569
+ );
570
+ const j = await r.json();
571
+ if (!r.ok || !j.message) throw new Error(j.reason ?? `http_${r.status}`);
572
+ setChallenge(j.message);
573
+ setStep("sign");
574
+ } catch (e) {
575
+ setErr(e instanceof Error ? e.message : "challenge failed");
576
+ } finally {
577
+ setBusy(false);
578
+ }
579
+ }
580
+ async function postLink(sig, confirmTransfer) {
581
+ setBusy(true);
582
+ setErr(null);
583
+ try {
584
+ const r = await fetch(`${authOrigin}/api/auth/link/btc`, {
585
+ method: "POST",
586
+ credentials: "include",
587
+ headers: { "Content-Type": "application/json" },
588
+ body: JSON.stringify({
589
+ message: challenge,
590
+ signature: sig,
591
+ ...confirmTransfer ? { confirm_transfer: true } : {}
592
+ })
593
+ });
594
+ const j = await r.json();
595
+ if (r.status === 409 && j.reason === "address_linked_elsewhere") {
596
+ setPendingSig(sig);
597
+ setBusy(false);
598
+ return;
599
+ }
600
+ if (handleSudoRequired(j, { purpose: "link a Bitcoin address" })) return;
601
+ if (isSudoAccountMismatch(j)) {
602
+ setErr(SUDO_ACCOUNT_MISMATCH_MESSAGE);
603
+ setBusy(false);
604
+ return;
605
+ }
606
+ if (!r.ok || !j.ok) throw new Error(j.reason ?? `http_${r.status}`);
607
+ onDone();
608
+ } catch (e) {
609
+ setErr(e instanceof Error ? e.message : "link failed");
610
+ setBusy(false);
611
+ }
612
+ }
613
+ return /* @__PURE__ */ jsxs("div", { style: panelStyle("accent"), "data-oc-li-form": "btc", children: [
614
+ /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 link a Bitcoin address" }),
615
+ step === "request" ? /* @__PURE__ */ jsxs(Fragment, { children: [
616
+ /* @__PURE__ */ jsx("p", { style: bodyStyle, children: "Enter a Bitcoin address you control. We generate a challenge and your wallet signs it (BIP-322)." }),
617
+ /* @__PURE__ */ jsx(
618
+ "input",
619
+ {
620
+ type: "text",
621
+ value: addr,
622
+ onChange: (e) => setAddr(e.target.value.trim()),
623
+ placeholder: "bc1q\u2026",
624
+ autoCapitalize: "none",
625
+ spellCheck: false,
626
+ style: { ...inputStyle, fontFamily: SIGNIN_FONT }
627
+ }
628
+ ),
629
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
630
+ /* @__PURE__ */ jsxs(FormButtons, { children: [
631
+ /* @__PURE__ */ jsx(
632
+ "button",
633
+ {
634
+ type: "button",
635
+ onClick: () => void getChallenge(),
636
+ disabled: busy || addr.length < 14,
637
+ style: primaryBtnStyle(busy || addr.length < 14),
638
+ children: busy ? "requesting\u2026" : "get challenge"
639
+ }
640
+ ),
641
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, style: ghostBtnStyle(busy), children: "cancel" })
642
+ ] })
643
+ ] }) : pendingSig ? /* @__PURE__ */ jsx(
644
+ TransferConfirm,
645
+ {
646
+ what: "bitcoin address",
647
+ busy,
648
+ onConfirm: () => void postLink(pendingSig, true),
649
+ onCancel,
650
+ err
651
+ }
652
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
653
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
654
+ "Sign the challenge with the wallet that controls",
655
+ " ",
656
+ /* @__PURE__ */ jsx("span", { style: { fontFamily: SIGNIN_FONT, wordBreak: "break-all" }, children: addr }),
657
+ "."
658
+ ] }),
659
+ /* @__PURE__ */ jsx(
660
+ LazyWalletButton,
661
+ {
662
+ address: addr,
663
+ message: challenge,
664
+ showManual: true,
665
+ layout: "list",
666
+ heading: null,
667
+ onSigned: (sig) => void postLink(sig, false),
668
+ onError: (e) => setErr(e?.message ?? "wallet rejected")
669
+ }
670
+ ),
671
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
672
+ /* @__PURE__ */ jsx(FormButtons, { children: /* @__PURE__ */ jsx(
673
+ "button",
674
+ {
675
+ type: "button",
676
+ onClick: () => {
677
+ setStep("request");
678
+ setErr(null);
679
+ },
680
+ disabled: busy,
681
+ style: ghostBtnStyle(busy),
682
+ children: "back"
683
+ }
684
+ ) })
685
+ ] })
686
+ ] });
687
+ }
688
+ function TransferConfirm({
689
+ what,
690
+ busy,
691
+ onConfirm,
692
+ onCancel,
693
+ err
694
+ }) {
695
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
696
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
697
+ "This ",
698
+ what,
699
+ " 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?"
700
+ ] }),
701
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
702
+ /* @__PURE__ */ jsxs(FormButtons, { children: [
703
+ /* @__PURE__ */ jsx(
704
+ "button",
705
+ {
706
+ type: "button",
707
+ onClick: onConfirm,
708
+ disabled: busy,
709
+ style: dangerBtnStyle(busy),
710
+ children: busy ? "transferring\u2026" : "transfer link here"
711
+ }
712
+ ),
713
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, disabled: busy, style: ghostBtnStyle(busy), children: "cancel" })
714
+ ] })
715
+ ] });
716
+ }
717
+ function LazyWalletButton(props) {
718
+ const [Comp, setComp] = React.useState(null);
719
+ const [failed, setFailed] = React.useState(false);
720
+ React.useEffect(() => {
721
+ let alive = true;
722
+ import("@orangecheck/wallet-adapter/react").then((m) => {
723
+ if (alive) {
724
+ setComp(
725
+ () => m.OcWalletButton
726
+ );
727
+ }
728
+ }).catch(() => {
729
+ if (alive) setFailed(true);
730
+ });
731
+ return () => {
732
+ alive = false;
733
+ };
734
+ }, []);
735
+ if (failed) {
736
+ return /* @__PURE__ */ jsx(ErrorLine, { children: "could not load wallet support \xB7 refresh to try again" });
737
+ }
738
+ if (!Comp) return /* @__PURE__ */ jsx("span", { style: hintStyle, children: "loading wallet options\u2026" });
739
+ return /* @__PURE__ */ jsx(Comp, { ...props });
740
+ }
741
+ function mask(value, kind) {
742
+ if (kind === "email") {
743
+ const [user, host] = value.split("@");
744
+ if (!user || !host) return "\u2022\u2022\u2022\u2022";
745
+ const m = user.length <= 2 ? "\u2022".repeat(user.length) : user[0] + "\u2022".repeat(user.length - 2) + user.slice(-1);
746
+ return `${m}@${host}`;
747
+ }
748
+ if (value.length <= 12) return "\u2022".repeat(value.length);
749
+ return `${value.slice(0, 6)}\u2026${value.slice(-4)}`;
750
+ }
751
+ function SectionLabel({
752
+ children,
753
+ tone = "primary"
754
+ }) {
755
+ const color = tone === "success" ? V.success : tone === "warn" ? V.warn : V.primary;
756
+ return /* @__PURE__ */ jsx(
757
+ "div",
758
+ {
759
+ style: {
760
+ color,
761
+ fontFamily: SIGNIN_FONT,
762
+ fontSize: signinSize(10),
763
+ letterSpacing: signinTracking("0.18em"),
764
+ textTransform: SIGNIN_CASE,
765
+ marginBottom: 8
766
+ },
767
+ children
768
+ }
769
+ );
770
+ }
771
+ function ErrorLine({ children }) {
772
+ return /* @__PURE__ */ jsx(
773
+ "p",
774
+ {
775
+ role: "alert",
776
+ style: {
777
+ margin: "8px 0 0",
778
+ color: V.destructive,
779
+ fontFamily: SIGNIN_FONT,
780
+ fontSize: signinSize(11)
781
+ },
782
+ children
783
+ }
784
+ );
785
+ }
786
+ function FormButtons({ children }) {
787
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, marginTop: 12 }, children });
788
+ }
789
+ var V = {
790
+ primary: "var(--primary, #f97316)",
791
+ foreground: "var(--foreground, #fafafa)",
792
+ muted: "var(--muted-foreground, #a1a1aa)",
793
+ background: "var(--background, #0a0a0a)",
794
+ card: "var(--card, #111113)",
795
+ border: "var(--border, #27272a)",
796
+ destructive: "var(--destructive, #ef4444)",
797
+ success: "var(--success, #22c55e)",
798
+ warn: "var(--warning, #eab308)"
799
+ };
800
+ function panelStyle(tone = "default") {
801
+ const base = {
802
+ background: V.card,
803
+ border: `1px solid ${V.border}`,
804
+ padding: "1.25rem"
805
+ };
806
+ if (tone === "plain") return { ...base, padding: 0 };
807
+ if (tone === "accent") return { ...base, borderColor: V.primary, background: "var(--card, #111113)" };
808
+ if (tone === "success") return { ...base, borderColor: V.success };
809
+ if (tone === "warn") return { ...base, borderColor: V.warn };
810
+ return base;
811
+ }
812
+ var bodyStyle = {
813
+ color: V.muted,
814
+ fontSize: 13,
815
+ lineHeight: 1.55,
816
+ margin: "0 0 12px"
817
+ };
818
+ var hintStyle = {
819
+ color: V.muted,
820
+ fontFamily: SIGNIN_FONT,
821
+ fontSize: signinSize(10.5),
822
+ opacity: 0.8
823
+ };
824
+ var rowStyle = {
825
+ display: "flex",
826
+ flexWrap: "wrap",
827
+ alignItems: "center",
828
+ justifyContent: "space-between",
829
+ gap: 12,
830
+ padding: "1rem"
831
+ };
832
+ var rowLabelStyle = {
833
+ color: V.foreground,
834
+ fontFamily: SIGNIN_FONT,
835
+ fontSize: signinSize(10.5),
836
+ letterSpacing: signinTracking("0.12em"),
837
+ textTransform: SIGNIN_CASE,
838
+ opacity: 0.8
839
+ };
840
+ var inputStyle = {
841
+ width: "100%",
842
+ padding: "0.55rem 0.75rem",
843
+ background: V.background,
844
+ color: V.foreground,
845
+ border: `1px solid ${V.border}`,
846
+ borderRadius: signinRadius(6),
847
+ fontFamily: SIGNIN_FONT,
848
+ // 16px — never lower · suppresses iOS Safari focus auto-zoom.
849
+ fontSize: 16,
850
+ boxSizing: "border-box"
851
+ };
852
+ function btnBase(disabled) {
853
+ return {
854
+ padding: "0.5rem 0.875rem",
855
+ borderRadius: SIGNIN_BUTTON_RADIUS,
856
+ fontFamily: SIGNIN_FONT,
857
+ fontSize: signinSize(10.5),
858
+ letterSpacing: signinTracking("0.12em"),
859
+ textTransform: SIGNIN_CASE,
860
+ cursor: disabled ? "not-allowed" : "pointer",
861
+ opacity: disabled ? 0.5 : 1
862
+ };
863
+ }
864
+ function primaryBtnStyle(disabled) {
865
+ return {
866
+ ...btnBase(disabled),
867
+ background: V.primary,
868
+ color: "var(--primary-foreground, #0a0a0a)",
869
+ border: `1px solid ${V.primary}`,
870
+ fontWeight: 600
871
+ };
872
+ }
873
+ function ghostBtnStyle(disabled) {
874
+ return {
875
+ ...btnBase(disabled),
876
+ background: V.background,
877
+ color: V.foreground,
878
+ border: `1px solid ${V.border}`
879
+ };
880
+ }
881
+ function dangerBtnStyle(disabled) {
882
+ return {
883
+ ...btnBase(disabled),
884
+ background: "transparent",
885
+ color: V.destructive,
886
+ border: `1px solid ${V.destructive}`
887
+ };
888
+ }
889
+ var primaryChipStyle = {
890
+ ...btnBase(false),
891
+ background: "transparent",
892
+ color: V.primary,
893
+ border: `1px solid ${V.primary}`
894
+ };
895
+
896
+ export {
897
+ fetchOcLinkedIdentities,
898
+ OcLinkedIdentities,
899
+ LinkPromptStep,
900
+ LazyWalletButton,
901
+ SectionLabel,
902
+ ErrorLine,
903
+ FormButtons,
904
+ V,
905
+ panelStyle,
906
+ bodyStyle,
907
+ hintStyle,
908
+ inputStyle,
909
+ primaryBtnStyle,
910
+ ghostBtnStyle,
911
+ dangerBtnStyle
912
+ };
913
+ //# sourceMappingURL=chunk-UFRU3I3D.mjs.map