@orangecheck/auth-client 2.5.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var React4 = require('react');
3
+ var React3 = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
  var browser = require('@simplewebauthn/browser');
6
6
 
@@ -22,7 +22,7 @@ function _interopNamespace(e) {
22
22
  return Object.freeze(n);
23
23
  }
24
24
 
25
- var React4__namespace = /*#__PURE__*/_interopNamespace(React4);
25
+ var React3__namespace = /*#__PURE__*/_interopNamespace(React3);
26
26
 
27
27
  // src/provider.tsx
28
28
 
@@ -43,7 +43,7 @@ function buildSignInUrl(cfg, returnTo) {
43
43
  u.searchParams.set("return_to", returnTo);
44
44
  return u.toString();
45
45
  }
46
- var SessionContext = React4__namespace.createContext(null);
46
+ var SessionContext = React3__namespace.createContext(null);
47
47
  function normalizeAccount(raw) {
48
48
  if (!raw) return null;
49
49
  const didOc = raw.did_oc ?? raw.didOc;
@@ -66,11 +66,11 @@ function OcSessionProvider({
66
66
  config,
67
67
  defaultReturnTo
68
68
  }) {
69
- const cfg = React4__namespace.useMemo(() => resolveConfig(config), [config]);
70
- const [account, setAccount] = React4__namespace.useState(null);
71
- const [status, setStatus] = React4__namespace.useState("loading");
72
- const [error, setError] = React4__namespace.useState(null);
73
- const refresh = React4__namespace.useCallback(async () => {
69
+ const cfg = React3__namespace.useMemo(() => resolveConfig(config), [config]);
70
+ const [account, setAccount] = React3__namespace.useState(null);
71
+ const [status, setStatus] = React3__namespace.useState("loading");
72
+ const [error, setError] = React3__namespace.useState(null);
73
+ const refresh = React3__namespace.useCallback(async () => {
74
74
  if (typeof window === "undefined") return;
75
75
  try {
76
76
  const res = await fetch(cfg.mePath, {
@@ -99,10 +99,10 @@ function OcSessionProvider({
99
99
  setError(err instanceof Error ? err : new Error(String(err)));
100
100
  }
101
101
  }, [cfg.mePath]);
102
- React4__namespace.useEffect(() => {
102
+ React3__namespace.useEffect(() => {
103
103
  void refresh();
104
104
  }, [refresh]);
105
- const signOut = React4__namespace.useCallback(async () => {
105
+ const signOut = React3__namespace.useCallback(async () => {
106
106
  try {
107
107
  await fetch(`${cfg.authOrigin}${cfg.logoutPath}`, {
108
108
  method: "POST",
@@ -113,7 +113,7 @@ function OcSessionProvider({
113
113
  setAccount(null);
114
114
  setStatus("anonymous");
115
115
  }, [cfg.authOrigin, cfg.logoutPath]);
116
- const value = React4__namespace.useMemo(() => {
116
+ const value = React3__namespace.useMemo(() => {
117
117
  const returnTo = defaultReturnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
118
118
  return {
119
119
  status,
@@ -127,7 +127,7 @@ function OcSessionProvider({
127
127
  return /* @__PURE__ */ jsxRuntime.jsx(SessionContext.Provider, { value, children });
128
128
  }
129
129
  function useOcSession() {
130
- const ctx = React4__namespace.useContext(SessionContext);
130
+ const ctx = React3__namespace.useContext(SessionContext);
131
131
  if (!ctx) {
132
132
  throw new Error(
133
133
  "[@orangecheck/auth-client] useOcSession() must be called inside <OcSessionProvider>"
@@ -136,7 +136,7 @@ function useOcSession() {
136
136
  return ctx;
137
137
  }
138
138
  function useOptionalOcSession() {
139
- return React4__namespace.useContext(SessionContext);
139
+ return React3__namespace.useContext(SessionContext);
140
140
  }
141
141
  function shortenAddress(addr) {
142
142
  if (addr.length <= 12) return addr;
@@ -151,7 +151,7 @@ function isPrefixOf(value, target) {
151
151
  }
152
152
  var listboxIdCounter = 0;
153
153
  function useUniqueId(prefix) {
154
- const [id] = React4__namespace.useState(() => `${prefix}-${++listboxIdCounter}`);
154
+ const [id] = React3__namespace.useState(() => `${prefix}-${++listboxIdCounter}`);
155
155
  return id;
156
156
  }
157
157
  function OcAccountChip({
@@ -165,9 +165,9 @@ function OcAccountChip({
165
165
  }) {
166
166
  const { status, account, signInUrl: sessionSignInUrl, signOut } = useOcSession();
167
167
  const signInUrl = signInUrlOverride ?? sessionSignInUrl;
168
- const [open, setOpen] = React4__namespace.useState(false);
169
- const wrapRef = React4__namespace.useRef(null);
170
- React4__namespace.useEffect(() => {
168
+ const [open, setOpen] = React3__namespace.useState(false);
169
+ const wrapRef = React3__namespace.useRef(null);
170
+ React3__namespace.useEffect(() => {
171
171
  if (!open) return;
172
172
  function handleClickOutside(e) {
173
173
  if (wrapRef.current && !wrapRef.current.contains(e.target)) {
@@ -490,9 +490,9 @@ function useOcAddressSuggestion(options) {
490
490
  } = options;
491
491
  const { status, account } = useOcSession();
492
492
  const sessionAddress = status === "authenticated" ? account?.primaryBtc ?? null : null;
493
- const [open, setOpen] = React4__namespace.useState(false);
494
- const [highlighted, setHighlighted] = React4__namespace.useState(false);
495
- const blurTimer = React4__namespace.useRef(null);
493
+ const [open, setOpen] = React3__namespace.useState(false);
494
+ const [highlighted, setHighlighted] = React3__namespace.useState(false);
495
+ const blurTimer = React3__namespace.useRef(null);
496
496
  const listboxId = useUniqueId("oc-addr-listbox");
497
497
  const optionId = `${listboxId}-opt`;
498
498
  const valueMatchesSession = sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();
@@ -504,7 +504,7 @@ function useOcAddressSuggestion(options) {
504
504
  setOpen(false);
505
505
  setHighlighted(false);
506
506
  }
507
- React4__namespace.useEffect(() => {
507
+ React3__namespace.useEffect(() => {
508
508
  return () => {
509
509
  if (blurTimer.current != null) window.clearTimeout(blurTimer.current);
510
510
  };
@@ -607,7 +607,7 @@ function useOcAddressSuggestion(options) {
607
607
  ) : null;
608
608
  return { inputProps, popover };
609
609
  }
610
- var OcAddressInput = React4__namespace.forwardRef(
610
+ var OcAddressInput = React3__namespace.forwardRef(
611
611
  function OcAddressInput2({
612
612
  value,
613
613
  onValueChange,
@@ -622,15 +622,15 @@ var OcAddressInput = React4__namespace.forwardRef(
622
622
  }, forwardedRef) {
623
623
  const { status, account } = useOcSession();
624
624
  const sessionAddress = status === "authenticated" ? account?.primaryBtc ?? null : null;
625
- const [open, setOpen] = React4__namespace.useState(false);
626
- const [highlighted, setHighlighted] = React4__namespace.useState(false);
627
- const innerRef = React4__namespace.useRef(null);
625
+ const [open, setOpen] = React3__namespace.useState(false);
626
+ const [highlighted, setHighlighted] = React3__namespace.useState(false);
627
+ const innerRef = React3__namespace.useRef(null);
628
628
  const setRef = (node) => {
629
629
  innerRef.current = node;
630
630
  if (typeof forwardedRef === "function") forwardedRef(node);
631
631
  else if (forwardedRef) forwardedRef.current = node;
632
632
  };
633
- const blurTimer = React4__namespace.useRef(null);
633
+ const blurTimer = React3__namespace.useRef(null);
634
634
  const listboxId = useUniqueId("oc-addr-listbox");
635
635
  const optionId = `${listboxId}-opt`;
636
636
  const valueMatchesSession = sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();
@@ -677,7 +677,7 @@ var OcAddressInput = React4__namespace.forwardRef(
677
677
  }
678
678
  onKeyDown?.(e);
679
679
  }
680
- React4__namespace.useEffect(() => {
680
+ React3__namespace.useEffect(() => {
681
681
  return () => {
682
682
  if (blurTimer.current != null) window.clearTimeout(blurTimer.current);
683
683
  };
@@ -770,6 +770,977 @@ var OcAddressInput = React4__namespace.forwardRef(
770
770
  );
771
771
  }
772
772
  );
773
+ var DEFAULT_AUTH_ORIGIN = "https://ochk.io";
774
+ function OcLinkedIdentities({
775
+ authOrigin = DEFAULT_AUTH_ORIGIN,
776
+ className,
777
+ onChange
778
+ }) {
779
+ const [resp, setResp] = React3__namespace.useState(null);
780
+ const [error, setError] = React3__namespace.useState(null);
781
+ const [showEmailLink, setShowEmailLink] = React3__namespace.useState(false);
782
+ const [showBtcLink, setShowBtcLink] = React3__namespace.useState(false);
783
+ const refresh = React3__namespace.useCallback(async () => {
784
+ setError(null);
785
+ try {
786
+ const r = await fetch(`${authOrigin}/api/auth/identities`, {
787
+ credentials: "include"
788
+ });
789
+ if (!r.ok) {
790
+ if (r.status === 401) {
791
+ setError("not signed in");
792
+ return;
793
+ }
794
+ throw new Error(`http_${r.status}`);
795
+ }
796
+ setResp(await r.json());
797
+ } catch (e) {
798
+ setError(e instanceof Error ? e.message : "load failed");
799
+ }
800
+ }, [authOrigin]);
801
+ React3__namespace.useEffect(() => {
802
+ void refresh();
803
+ }, [refresh]);
804
+ const afterChange = React3__namespace.useCallback(() => {
805
+ setShowEmailLink(false);
806
+ setShowBtcLink(false);
807
+ void refresh();
808
+ onChange?.();
809
+ }, [refresh, onChange]);
810
+ if (error) {
811
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, "data-oc-li": "", style: panelStyle("warn"), role: "alert", children: [
812
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { tone: "warn", children: "\xA7 identities \xB7 load deferred" }),
813
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: bodyStyle, children: [
814
+ "Couldn't reach the auth host (",
815
+ error,
816
+ "). Linking is temporarily unavailable; your signed-in identity still works as normal."
817
+ ] }),
818
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void refresh(), style: ghostBtnStyle(false), children: "retry" })
819
+ ] });
820
+ }
821
+ if (!resp) {
822
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, "data-oc-li": "", style: panelStyle(), children: /* @__PURE__ */ jsxRuntime.jsxs("span", { style: hintStyle, children: [
823
+ "> ",
824
+ "loading\u2026"
825
+ ] }) });
826
+ }
827
+ const linked = resp.linked ?? [];
828
+ const emailRows = linked.filter((i) => i.kind === "email");
829
+ const btcRows = linked.filter((i) => i.kind === "btc");
830
+ const hasEmail = emailRows.some((i) => i.value);
831
+ const hasBtc = btcRows.some((i) => i.value);
832
+ const mergedFrom = resp.merged_from ?? [];
833
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, "data-oc-li": "", style: { display: "grid", gap: 16 }, children: [
834
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panelStyle(), children: [
835
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "\xA7 what this is" }),
836
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: bodyStyle, children: [
837
+ "Your public identity is the opaque ",
838
+ /* @__PURE__ */ jsxRuntime.jsx("code", { style: codeStyle, children: "did:oc:" }),
839
+ " DID. Below it sit your private linked identities \u2014 your email and your Bitcoin address. They never appear in dashboard chrome and never reach an integrator unless you grant the matching scope. Linking a second identity is also your account-recovery path: if you lose one, the other still signs you in."
840
+ ] })
841
+ ] }),
842
+ mergedFrom.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ConsolidationPanel, { entries: mergedFrom }),
843
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panelStyle("plain"), "data-oc-li-rows": "", children: [
844
+ /* @__PURE__ */ jsxRuntime.jsx(
845
+ IdentityGroup,
846
+ {
847
+ kind: "email",
848
+ title: "email",
849
+ rows: emailRows,
850
+ authOrigin,
851
+ onChange: afterChange,
852
+ addCta: hasEmail ? null : "link an email",
853
+ onAdd: () => {
854
+ setShowEmailLink((v) => !v);
855
+ setShowBtcLink(false);
856
+ }
857
+ }
858
+ ),
859
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { borderTop: `1px solid ${V.border}` } }),
860
+ /* @__PURE__ */ jsxRuntime.jsx(
861
+ IdentityGroup,
862
+ {
863
+ kind: "btc",
864
+ title: "bitcoin address",
865
+ rows: btcRows,
866
+ authOrigin,
867
+ onChange: afterChange,
868
+ addCta: hasBtc ? null : "link a Bitcoin address",
869
+ onAdd: () => {
870
+ setShowBtcLink((v) => !v);
871
+ setShowEmailLink(false);
872
+ }
873
+ }
874
+ )
875
+ ] }),
876
+ showEmailLink && /* @__PURE__ */ jsxRuntime.jsx(
877
+ EmailLinkForm,
878
+ {
879
+ authOrigin,
880
+ onDone: afterChange,
881
+ onCancel: () => setShowEmailLink(false)
882
+ }
883
+ ),
884
+ showBtcLink && /* @__PURE__ */ jsxRuntime.jsx(
885
+ BtcLinkForm,
886
+ {
887
+ authOrigin,
888
+ didOc: resp.did_oc ?? null,
889
+ onDone: afterChange,
890
+ onCancel: () => setShowBtcLink(false)
891
+ }
892
+ ),
893
+ hasBtc && resp.did_oc && /* @__PURE__ */ jsxRuntime.jsx(OcIdentityBond, { didOc: resp.did_oc, btc: btcRows.find((i) => !!i.value).value })
894
+ ] });
895
+ }
896
+ function IdentityGroup({
897
+ kind,
898
+ title,
899
+ rows,
900
+ authOrigin,
901
+ onChange,
902
+ addCta,
903
+ onAdd
904
+ }) {
905
+ if (rows.length === 0) {
906
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: rowStyle, children: [
907
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
908
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: rowLabelStyle, children: [
909
+ "\xA7 ",
910
+ title
911
+ ] }),
912
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...bodyStyle, marginTop: 4 }, children: "none linked" })
913
+ ] }),
914
+ addCta && /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: onAdd, style: primaryChipStyle, children: [
915
+ "+ ",
916
+ addCta
917
+ ] })
918
+ ] });
919
+ }
920
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: rows.map((r) => /* @__PURE__ */ jsxRuntime.jsx(
921
+ IdentityRow,
922
+ {
923
+ row: r,
924
+ kind,
925
+ authOrigin,
926
+ onChange
927
+ },
928
+ r.id
929
+ )) });
930
+ }
931
+ function IdentityRow({
932
+ row,
933
+ kind,
934
+ authOrigin,
935
+ onChange
936
+ }) {
937
+ const [shown, setShown] = React3__namespace.useState(false);
938
+ const [confirming, setConfirming] = React3__namespace.useState(false);
939
+ const [busy, setBusy] = React3__namespace.useState(false);
940
+ const [err, setErr] = React3__namespace.useState(null);
941
+ const masked = row.value ? mask(row.value, kind) : "\u2014 pending recovery \u2014";
942
+ const display = shown && row.value ? row.value : masked;
943
+ async function unlink() {
944
+ if (row.is_primary) return;
945
+ setBusy(true);
946
+ setErr(null);
947
+ try {
948
+ const r = await fetch(
949
+ `${authOrigin}/api/auth/identities?id=${encodeURIComponent(row.id)}`,
950
+ { method: "DELETE", credentials: "include" }
951
+ );
952
+ if (!r.ok) {
953
+ const j = await r.json().catch(() => ({}));
954
+ throw new Error(j.reason ?? `http_${r.status}`);
955
+ }
956
+ onChange();
957
+ } catch (e) {
958
+ setErr(e instanceof Error ? e.message : "unlink failed");
959
+ setBusy(false);
960
+ setConfirming(false);
961
+ }
962
+ }
963
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: rowStyle, "data-oc-li-row": kind, children: [
964
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minWidth: 0, flex: 1 }, children: [
965
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: "4px 12px" }, children: [
966
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: rowLabelStyle, children: [
967
+ "\xA7 ",
968
+ kind === "email" ? "email" : "bitcoin address"
969
+ ] }),
970
+ row.is_primary && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...rowLabelStyle, color: V.primary }, children: "primary \xB7 signup-time" })
971
+ ] }),
972
+ /* @__PURE__ */ jsxRuntime.jsx(
973
+ "div",
974
+ {
975
+ style: {
976
+ marginTop: 4,
977
+ color: V.foreground,
978
+ fontFamily: MONO,
979
+ fontSize: 12,
980
+ wordBreak: "break-all"
981
+ },
982
+ children: display
983
+ }
984
+ ),
985
+ row.verified_at && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...hintStyle, marginTop: 4 }, children: [
986
+ "verified \xB7 ",
987
+ new Date(row.verified_at).toUTCString()
988
+ ] }),
989
+ err && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine, { children: err })
990
+ ] }),
991
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexShrink: 0, gap: 8, flexWrap: "wrap" }, children: [
992
+ row.value && /* @__PURE__ */ jsxRuntime.jsx(
993
+ "button",
994
+ {
995
+ type: "button",
996
+ onClick: () => setShown((s) => !s),
997
+ style: ghostBtnStyle(false),
998
+ children: shown ? "hide" : "show"
999
+ }
1000
+ ),
1001
+ !row.is_primary && !confirming && /* @__PURE__ */ jsxRuntime.jsx(
1002
+ "button",
1003
+ {
1004
+ type: "button",
1005
+ onClick: () => setConfirming(true),
1006
+ style: dangerBtnStyle(false),
1007
+ children: "unlink"
1008
+ }
1009
+ ),
1010
+ !row.is_primary && confirming && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1011
+ /* @__PURE__ */ jsxRuntime.jsx(
1012
+ "button",
1013
+ {
1014
+ type: "button",
1015
+ onClick: () => void unlink(),
1016
+ disabled: busy,
1017
+ style: dangerBtnStyle(busy),
1018
+ children: busy ? "unlinking\u2026" : "confirm unlink"
1019
+ }
1020
+ ),
1021
+ /* @__PURE__ */ jsxRuntime.jsx(
1022
+ "button",
1023
+ {
1024
+ type: "button",
1025
+ onClick: () => setConfirming(false),
1026
+ disabled: busy,
1027
+ style: ghostBtnStyle(busy),
1028
+ children: "cancel"
1029
+ }
1030
+ )
1031
+ ] })
1032
+ ] })
1033
+ ] });
1034
+ }
1035
+ function ConsolidationPanel({ entries }) {
1036
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panelStyle("success"), "data-oc-li-consolidated": "", children: [
1037
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionLabel, { tone: "success", children: [
1038
+ "\xA7 consolidated accounts \xB7 ",
1039
+ entries.length
1040
+ ] }),
1041
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: bodyStyle, children: [
1042
+ entries.length === 1 ? "A previous OC account" : "Previous OC accounts",
1043
+ " you signed up with, folded into this one under dual proof of control. Historical activity is unioned into this account automatically;",
1044
+ " ",
1045
+ entries.length === 1 ? "it" : "they",
1046
+ " can no longer sign in."
1047
+ ] }),
1048
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { style: { listStyle: "none", margin: "12px 0 0", padding: 0 }, children: entries.map((e) => /* @__PURE__ */ jsxRuntime.jsxs(
1049
+ "li",
1050
+ {
1051
+ style: {
1052
+ display: "flex",
1053
+ flexWrap: "wrap",
1054
+ gap: 8,
1055
+ padding: "8px 0",
1056
+ borderTop: `1px solid ${V.border}`,
1057
+ fontFamily: MONO,
1058
+ fontSize: 11
1059
+ },
1060
+ children: [
1061
+ /* @__PURE__ */ jsxRuntime.jsx("code", { style: { color: V.foreground, wordBreak: "break-all" }, children: e.did_oc }),
1062
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { ...hintStyle, marginLeft: "auto" }, children: [
1063
+ "created ",
1064
+ new Date(e.created_at).toLocaleDateString()
1065
+ ] })
1066
+ ]
1067
+ },
1068
+ e.did_oc
1069
+ )) })
1070
+ ] });
1071
+ }
1072
+ function EmailLinkForm({
1073
+ authOrigin,
1074
+ onDone,
1075
+ onCancel
1076
+ }) {
1077
+ const [step, setStep] = React3__namespace.useState("email");
1078
+ const [email, setEmail] = React3__namespace.useState("");
1079
+ const [code, setCode] = React3__namespace.useState("");
1080
+ const [token, setToken] = React3__namespace.useState("");
1081
+ const [busy, setBusy] = React3__namespace.useState(false);
1082
+ const [err, setErr] = React3__namespace.useState(null);
1083
+ const [transfer, setTransfer] = React3__namespace.useState(false);
1084
+ async function start() {
1085
+ setBusy(true);
1086
+ setErr(null);
1087
+ try {
1088
+ const r = await fetch(`${authOrigin}/api/auth/email-otp/start`, {
1089
+ method: "POST",
1090
+ credentials: "include",
1091
+ headers: { "Content-Type": "application/json" },
1092
+ body: JSON.stringify({ email: email.trim().toLowerCase() })
1093
+ });
1094
+ const j = await r.json();
1095
+ if (!r.ok || !j.token) throw new Error(j.reason ?? `http_${r.status}`);
1096
+ setToken(j.token);
1097
+ setStep("code");
1098
+ } catch (e) {
1099
+ setErr(e instanceof Error ? e.message : "could not send code");
1100
+ } finally {
1101
+ setBusy(false);
1102
+ }
1103
+ }
1104
+ async function verify(confirmTransfer) {
1105
+ setBusy(true);
1106
+ setErr(null);
1107
+ try {
1108
+ const r = await fetch(`${authOrigin}/api/auth/link/email`, {
1109
+ method: "POST",
1110
+ credentials: "include",
1111
+ headers: { "Content-Type": "application/json" },
1112
+ body: JSON.stringify({
1113
+ email: email.trim().toLowerCase(),
1114
+ code: code.trim(),
1115
+ token,
1116
+ ...confirmTransfer ? { confirm_transfer: true } : {}
1117
+ })
1118
+ });
1119
+ const j = await r.json();
1120
+ if (r.status === 409 && j.reason === "email_linked_elsewhere") {
1121
+ setTransfer(true);
1122
+ setBusy(false);
1123
+ return;
1124
+ }
1125
+ if (!r.ok || !j.ok) throw new Error(j.reason ?? `http_${r.status}`);
1126
+ onDone();
1127
+ } catch (e) {
1128
+ setErr(e instanceof Error ? e.message : "link failed");
1129
+ setBusy(false);
1130
+ }
1131
+ }
1132
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panelStyle("accent"), "data-oc-li-form": "email", children: [
1133
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "\xA7 link an email" }),
1134
+ step === "email" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1135
+ /* @__PURE__ */ jsxRuntime.jsx(
1136
+ "input",
1137
+ {
1138
+ type: "email",
1139
+ autoComplete: "email",
1140
+ value: email,
1141
+ onChange: (e) => setEmail(e.target.value),
1142
+ placeholder: "you@example.com",
1143
+ style: inputStyle
1144
+ }
1145
+ ),
1146
+ err && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine, { children: err }),
1147
+ /* @__PURE__ */ jsxRuntime.jsxs(FormButtons, { children: [
1148
+ /* @__PURE__ */ jsxRuntime.jsx(
1149
+ "button",
1150
+ {
1151
+ type: "button",
1152
+ onClick: () => void start(),
1153
+ disabled: busy || email.trim().length < 5,
1154
+ style: primaryBtnStyle(busy || email.trim().length < 5),
1155
+ children: busy ? "sending\u2026" : "send code"
1156
+ }
1157
+ ),
1158
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onCancel, style: ghostBtnStyle(busy), children: "cancel" })
1159
+ ] })
1160
+ ] }) : transfer ? /* @__PURE__ */ jsxRuntime.jsx(
1161
+ TransferConfirm,
1162
+ {
1163
+ what: "email",
1164
+ busy,
1165
+ onConfirm: () => void verify(true),
1166
+ onCancel,
1167
+ err
1168
+ }
1169
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1170
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: bodyStyle, children: [
1171
+ "Enter the 6-digit code sent to ",
1172
+ email,
1173
+ "."
1174
+ ] }),
1175
+ /* @__PURE__ */ jsxRuntime.jsx(
1176
+ "input",
1177
+ {
1178
+ type: "text",
1179
+ inputMode: "numeric",
1180
+ autoComplete: "one-time-code",
1181
+ maxLength: 6,
1182
+ value: code,
1183
+ onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
1184
+ placeholder: "123456",
1185
+ style: { ...inputStyle, letterSpacing: "0.3em" }
1186
+ }
1187
+ ),
1188
+ err && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine, { children: err }),
1189
+ /* @__PURE__ */ jsxRuntime.jsxs(FormButtons, { children: [
1190
+ /* @__PURE__ */ jsxRuntime.jsx(
1191
+ "button",
1192
+ {
1193
+ type: "button",
1194
+ onClick: () => void verify(false),
1195
+ disabled: busy || code.length !== 6,
1196
+ style: primaryBtnStyle(busy || code.length !== 6),
1197
+ children: busy ? "linking\u2026" : "verify + link"
1198
+ }
1199
+ ),
1200
+ /* @__PURE__ */ jsxRuntime.jsx(
1201
+ "button",
1202
+ {
1203
+ type: "button",
1204
+ onClick: () => setStep("email"),
1205
+ disabled: busy,
1206
+ style: ghostBtnStyle(busy),
1207
+ children: "back"
1208
+ }
1209
+ )
1210
+ ] })
1211
+ ] })
1212
+ ] });
1213
+ }
1214
+ function BtcLinkForm({
1215
+ authOrigin,
1216
+ didOc,
1217
+ onDone,
1218
+ onCancel
1219
+ }) {
1220
+ const [step, setStep] = React3__namespace.useState("request");
1221
+ const [addr, setAddr] = React3__namespace.useState("");
1222
+ const [challenge, setChallenge] = React3__namespace.useState("");
1223
+ const [busy, setBusy] = React3__namespace.useState(false);
1224
+ const [err, setErr] = React3__namespace.useState(null);
1225
+ const [pendingSig, setPendingSig] = React3__namespace.useState(null);
1226
+ async function getChallenge() {
1227
+ if (!didOc) {
1228
+ setErr("session not loaded yet \xB7 refresh and retry");
1229
+ return;
1230
+ }
1231
+ setBusy(true);
1232
+ setErr(null);
1233
+ try {
1234
+ const r = await fetch(
1235
+ `${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&purpose=link&audience=${encodeURIComponent(didOc)}`
1236
+ );
1237
+ const j = await r.json();
1238
+ if (!r.ok || !j.message) throw new Error(j.reason ?? `http_${r.status}`);
1239
+ setChallenge(j.message);
1240
+ setStep("sign");
1241
+ } catch (e) {
1242
+ setErr(e instanceof Error ? e.message : "challenge failed");
1243
+ } finally {
1244
+ setBusy(false);
1245
+ }
1246
+ }
1247
+ async function postLink(sig, confirmTransfer) {
1248
+ setBusy(true);
1249
+ setErr(null);
1250
+ try {
1251
+ const r = await fetch(`${authOrigin}/api/auth/link/btc`, {
1252
+ method: "POST",
1253
+ credentials: "include",
1254
+ headers: { "Content-Type": "application/json" },
1255
+ body: JSON.stringify({
1256
+ message: challenge,
1257
+ signature: sig,
1258
+ ...confirmTransfer ? { confirm_transfer: true } : {}
1259
+ })
1260
+ });
1261
+ const j = await r.json();
1262
+ if (r.status === 409 && j.reason === "address_linked_elsewhere") {
1263
+ setPendingSig(sig);
1264
+ setBusy(false);
1265
+ return;
1266
+ }
1267
+ if (!r.ok || !j.ok) throw new Error(j.reason ?? `http_${r.status}`);
1268
+ onDone();
1269
+ } catch (e) {
1270
+ setErr(e instanceof Error ? e.message : "link failed");
1271
+ setBusy(false);
1272
+ }
1273
+ }
1274
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panelStyle("accent"), "data-oc-li-form": "btc", children: [
1275
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "\xA7 link a Bitcoin address" }),
1276
+ step === "request" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1277
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: bodyStyle, children: "Enter a Bitcoin address you control. We generate a challenge and your wallet signs it (BIP-322)." }),
1278
+ /* @__PURE__ */ jsxRuntime.jsx(
1279
+ "input",
1280
+ {
1281
+ type: "text",
1282
+ value: addr,
1283
+ onChange: (e) => setAddr(e.target.value.trim()),
1284
+ placeholder: "bc1q\u2026",
1285
+ autoCapitalize: "none",
1286
+ spellCheck: false,
1287
+ style: { ...inputStyle, fontFamily: MONO }
1288
+ }
1289
+ ),
1290
+ err && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine, { children: err }),
1291
+ /* @__PURE__ */ jsxRuntime.jsxs(FormButtons, { children: [
1292
+ /* @__PURE__ */ jsxRuntime.jsx(
1293
+ "button",
1294
+ {
1295
+ type: "button",
1296
+ onClick: () => void getChallenge(),
1297
+ disabled: busy || addr.length < 14,
1298
+ style: primaryBtnStyle(busy || addr.length < 14),
1299
+ children: busy ? "requesting\u2026" : "get challenge"
1300
+ }
1301
+ ),
1302
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onCancel, style: ghostBtnStyle(busy), children: "cancel" })
1303
+ ] })
1304
+ ] }) : pendingSig ? /* @__PURE__ */ jsxRuntime.jsx(
1305
+ TransferConfirm,
1306
+ {
1307
+ what: "bitcoin address",
1308
+ busy,
1309
+ onConfirm: () => void postLink(pendingSig, true),
1310
+ onCancel,
1311
+ err
1312
+ }
1313
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1314
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: bodyStyle, children: [
1315
+ "Sign the challenge with the wallet that controls",
1316
+ " ",
1317
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontFamily: MONO, wordBreak: "break-all" }, children: addr }),
1318
+ "."
1319
+ ] }),
1320
+ /* @__PURE__ */ jsxRuntime.jsx(
1321
+ LazyWalletButton,
1322
+ {
1323
+ address: addr,
1324
+ message: challenge,
1325
+ showManual: true,
1326
+ layout: "list",
1327
+ heading: null,
1328
+ onSigned: (sig) => void postLink(sig, false),
1329
+ onError: (e) => setErr(e?.message ?? "wallet rejected")
1330
+ }
1331
+ ),
1332
+ err && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine, { children: err }),
1333
+ /* @__PURE__ */ jsxRuntime.jsx(FormButtons, { children: /* @__PURE__ */ jsxRuntime.jsx(
1334
+ "button",
1335
+ {
1336
+ type: "button",
1337
+ onClick: () => {
1338
+ setStep("request");
1339
+ setErr(null);
1340
+ },
1341
+ disabled: busy,
1342
+ style: ghostBtnStyle(busy),
1343
+ children: "back"
1344
+ }
1345
+ ) })
1346
+ ] })
1347
+ ] });
1348
+ }
1349
+ function TransferConfirm({
1350
+ what,
1351
+ busy,
1352
+ onConfirm,
1353
+ onCancel,
1354
+ err
1355
+ }) {
1356
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1357
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: bodyStyle, children: [
1358
+ "This ",
1359
+ what,
1360
+ " 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?"
1361
+ ] }),
1362
+ err && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine, { children: err }),
1363
+ /* @__PURE__ */ jsxRuntime.jsxs(FormButtons, { children: [
1364
+ /* @__PURE__ */ jsxRuntime.jsx(
1365
+ "button",
1366
+ {
1367
+ type: "button",
1368
+ onClick: onConfirm,
1369
+ disabled: busy,
1370
+ style: dangerBtnStyle(busy),
1371
+ children: busy ? "transferring\u2026" : "transfer link here"
1372
+ }
1373
+ ),
1374
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onCancel, disabled: busy, style: ghostBtnStyle(busy), children: "cancel" })
1375
+ ] })
1376
+ ] });
1377
+ }
1378
+ function OcIdentityBond({
1379
+ didOc,
1380
+ btc,
1381
+ className
1382
+ }) {
1383
+ const [stage, setStage] = React3__namespace.useState("idle");
1384
+ const [message, setMessage] = React3__namespace.useState("");
1385
+ const [err, setErr] = React3__namespace.useState(null);
1386
+ const [result, setResult] = React3__namespace.useState(null);
1387
+ function nip07() {
1388
+ if (typeof window === "undefined") return null;
1389
+ const w = window;
1390
+ return w.nostr ?? null;
1391
+ }
1392
+ async function begin() {
1393
+ setErr(null);
1394
+ const nostr = nip07();
1395
+ if (!nostr) {
1396
+ setErr(
1397
+ "no Nostr signer found \xB7 install a NIP-07 extension (Alby, nos2x) to counter-sign the bond"
1398
+ );
1399
+ return;
1400
+ }
1401
+ try {
1402
+ const sdk = await loadSdk();
1403
+ const pubkeyHex = (await nostr.getPublicKey()).toLowerCase();
1404
+ const npub = sdk.xOnlyHexToNpub(pubkeyHex);
1405
+ const msg = sdk.buildBindingMessage({
1406
+ principal: didOc,
1407
+ btc,
1408
+ nostr: npub,
1409
+ nonce: randomNonceHex(),
1410
+ issued_at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z")
1411
+ });
1412
+ setMessage(msg);
1413
+ setStage("sign-btc");
1414
+ } catch (e) {
1415
+ setErr(e instanceof Error ? e.message : "could not start the bond ceremony");
1416
+ }
1417
+ }
1418
+ async function afterBtcSign(btcSignature) {
1419
+ setErr(null);
1420
+ setStage("sign-nostr");
1421
+ try {
1422
+ const nostr = nip07();
1423
+ if (!nostr) throw new Error("Nostr extension went away \xB7 retry");
1424
+ const sdk = await loadSdk();
1425
+ const pubkeyHex = (await nostr.getPublicKey()).toLowerCase();
1426
+ const template = sdk.buildBindingEventTemplate({
1427
+ message,
1428
+ btcSignature,
1429
+ nostrPubkeyHex: pubkeyHex,
1430
+ btc
1431
+ });
1432
+ const signedEvent = await nostr.signEvent(template);
1433
+ const envelope = sdk.assembleBindingEnvelope({
1434
+ message,
1435
+ btcSignature,
1436
+ btcScheme: "bip322",
1437
+ nostrEvent: signedEvent
1438
+ });
1439
+ const check = sdk.verifyBinding(envelope);
1440
+ if (!check.valid) {
1441
+ throw new Error(`assembled bond failed local verification (${check.status})`);
1442
+ }
1443
+ setStage("publishing");
1444
+ const { publishEvent } = await loadNostrCore();
1445
+ const results = await publishEvent(signedEvent);
1446
+ const ok = results.filter((r) => r.ok).length;
1447
+ if (ok === 0) {
1448
+ throw new Error("no relay accepted the bond \xB7 check your connection and retry");
1449
+ }
1450
+ setResult({
1451
+ bindingId: check.valid ? check.binding_id : sdk.bindingId(message),
1452
+ relays: ok,
1453
+ relaysTotal: results.length
1454
+ });
1455
+ setStage("done");
1456
+ } catch (e) {
1457
+ setErr(e instanceof Error ? e.message : "bond publish failed");
1458
+ setStage("sign-btc");
1459
+ }
1460
+ }
1461
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: panelStyle("accent"), "data-oc-identity-bond": "", children: [
1462
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "\xA7 identity bond \xB7 publish to nostr" }),
1463
+ stage === "idle" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1464
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: bodyStyle, children: [
1465
+ "Publish a ",
1466
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Binding Attestation" }),
1467
+ " \u2014 a Bitcoin-signed, Nostr-signed proof that this address and your Nostr key are one identity. It lives on Nostr, verifies with zero trust in OrangeCheck, and means your identity survives even if OC's database does not. Two signatures: your Bitcoin wallet, then your Nostr extension."
1468
+ ] }),
1469
+ /* @__PURE__ */ jsxRuntime.jsx(FormButtons, { children: /* @__PURE__ */ jsxRuntime.jsx(
1470
+ "button",
1471
+ {
1472
+ type: "button",
1473
+ onClick: () => void begin(),
1474
+ style: primaryBtnStyle(false),
1475
+ children: "create & publish bond"
1476
+ }
1477
+ ) })
1478
+ ] }),
1479
+ stage === "sign-btc" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1480
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: bodyStyle, children: [
1481
+ "Step 1 of 2 \u2014 sign the binding message with the Bitcoin wallet that controls ",
1482
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontFamily: MONO, wordBreak: "break-all" }, children: btc }),
1483
+ "."
1484
+ ] }),
1485
+ /* @__PURE__ */ jsxRuntime.jsx(
1486
+ LazyWalletButton,
1487
+ {
1488
+ address: btc,
1489
+ message,
1490
+ showManual: true,
1491
+ layout: "list",
1492
+ heading: null,
1493
+ onSigned: (sig) => void afterBtcSign(sig),
1494
+ onError: (e) => setErr(e?.message ?? "wallet rejected the signature")
1495
+ }
1496
+ )
1497
+ ] }),
1498
+ (stage === "sign-nostr" || stage === "publishing") && /* @__PURE__ */ jsxRuntime.jsx("p", { style: bodyStyle, children: stage === "sign-nostr" ? "Step 2 of 2 \u2014 approve the signature in your Nostr extension\u2026" : "Publishing the bond to Nostr\u2026" }),
1499
+ stage === "done" && result && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1500
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { tone: "success", children: "\xA7 bond published" }),
1501
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: bodyStyle, children: [
1502
+ "Your identity bond is live on ",
1503
+ result.relays,
1504
+ "/",
1505
+ result.relaysTotal,
1506
+ " ",
1507
+ "relay",
1508
+ result.relays === 1 ? "" : "s",
1509
+ ". Anyone can now verify the BTC \u21C4 Nostr binding offline."
1510
+ ] }),
1511
+ /* @__PURE__ */ jsxRuntime.jsxs(
1512
+ "div",
1513
+ {
1514
+ style: {
1515
+ fontFamily: MONO,
1516
+ fontSize: 11,
1517
+ color: V.muted,
1518
+ wordBreak: "break-all"
1519
+ },
1520
+ children: [
1521
+ "binding_id: ",
1522
+ result.bindingId
1523
+ ]
1524
+ }
1525
+ )
1526
+ ] }),
1527
+ err && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine, { children: err })
1528
+ ] });
1529
+ }
1530
+ function randomNonceHex() {
1531
+ const b = new Uint8Array(16);
1532
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
1533
+ crypto.getRandomValues(b);
1534
+ } else {
1535
+ for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);
1536
+ }
1537
+ let out = "";
1538
+ for (const x of b) out += x.toString(16).padStart(2, "0");
1539
+ return out;
1540
+ }
1541
+ async function loadSdk() {
1542
+ try {
1543
+ return await import('@orangecheck/sdk');
1544
+ } catch {
1545
+ throw new Error("@orangecheck/sdk not installed \xB7 add it to your site to publish bonds");
1546
+ }
1547
+ }
1548
+ async function loadNostrCore() {
1549
+ try {
1550
+ return await import('@orangecheck/nostr-core');
1551
+ } catch {
1552
+ throw new Error(
1553
+ "@orangecheck/nostr-core not installed \xB7 add it to your site to publish bonds"
1554
+ );
1555
+ }
1556
+ }
1557
+ function LazyWalletButton(props) {
1558
+ const [Comp, setComp] = React3__namespace.useState(null);
1559
+ const [failed, setFailed] = React3__namespace.useState(false);
1560
+ React3__namespace.useEffect(() => {
1561
+ let alive = true;
1562
+ import('@orangecheck/wallet-adapter/react').then((m) => {
1563
+ if (alive) {
1564
+ setComp(
1565
+ () => m.OcWalletButton
1566
+ );
1567
+ }
1568
+ }).catch(() => {
1569
+ if (alive) setFailed(true);
1570
+ });
1571
+ return () => {
1572
+ alive = false;
1573
+ };
1574
+ }, []);
1575
+ if (failed) {
1576
+ return /* @__PURE__ */ jsxRuntime.jsx(ErrorLine, { children: "@orangecheck/wallet-adapter not installed \xB7 add it to your site to link a wallet" });
1577
+ }
1578
+ if (!Comp) return /* @__PURE__ */ jsxRuntime.jsxs("span", { style: hintStyle, children: [
1579
+ "> ",
1580
+ "loading wallet options\u2026"
1581
+ ] });
1582
+ return /* @__PURE__ */ jsxRuntime.jsx(Comp, { ...props });
1583
+ }
1584
+ function mask(value, kind) {
1585
+ if (kind === "email") {
1586
+ const [user, host] = value.split("@");
1587
+ if (!user || !host) return "\u2022\u2022\u2022\u2022";
1588
+ const m = user.length <= 2 ? "\u2022".repeat(user.length) : user[0] + "\u2022".repeat(user.length - 2) + user.slice(-1);
1589
+ return `${m}@${host}`;
1590
+ }
1591
+ if (value.length <= 12) return "\u2022".repeat(value.length);
1592
+ return `${value.slice(0, 6)}\u2026${value.slice(-4)}`;
1593
+ }
1594
+ function SectionLabel({
1595
+ children,
1596
+ tone = "primary"
1597
+ }) {
1598
+ const color = tone === "success" ? V.success : tone === "warn" ? V.warn : V.primary;
1599
+ return /* @__PURE__ */ jsxRuntime.jsx(
1600
+ "div",
1601
+ {
1602
+ style: {
1603
+ color,
1604
+ fontFamily: MONO,
1605
+ fontSize: 10,
1606
+ letterSpacing: "0.18em",
1607
+ textTransform: "uppercase",
1608
+ marginBottom: 8
1609
+ },
1610
+ children
1611
+ }
1612
+ );
1613
+ }
1614
+ function ErrorLine({ children }) {
1615
+ return /* @__PURE__ */ jsxRuntime.jsx(
1616
+ "p",
1617
+ {
1618
+ role: "alert",
1619
+ style: {
1620
+ margin: "8px 0 0",
1621
+ color: V.destructive,
1622
+ fontFamily: MONO,
1623
+ fontSize: 11
1624
+ },
1625
+ children
1626
+ }
1627
+ );
1628
+ }
1629
+ function FormButtons({ children }) {
1630
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, marginTop: 12 }, children });
1631
+ }
1632
+ var MONO = "ui-monospace, SFMono-Regular, monospace";
1633
+ var V = {
1634
+ primary: "var(--primary, #f97316)",
1635
+ foreground: "var(--foreground, #fafafa)",
1636
+ muted: "var(--muted-foreground, #a1a1aa)",
1637
+ background: "var(--background, #0a0a0a)",
1638
+ card: "var(--card, #111113)",
1639
+ border: "var(--border, #27272a)",
1640
+ destructive: "var(--destructive, #ef4444)",
1641
+ success: "var(--success, #22c55e)",
1642
+ warn: "var(--warning, #eab308)"
1643
+ };
1644
+ function panelStyle(tone = "default") {
1645
+ const base = {
1646
+ background: V.card,
1647
+ border: `1px solid ${V.border}`,
1648
+ padding: "1.25rem"
1649
+ };
1650
+ if (tone === "plain") return { ...base, padding: 0 };
1651
+ if (tone === "accent") return { ...base, borderColor: V.primary, background: "var(--card, #111113)" };
1652
+ if (tone === "success") return { ...base, borderColor: V.success };
1653
+ if (tone === "warn") return { ...base, borderColor: V.warn };
1654
+ return base;
1655
+ }
1656
+ var bodyStyle = {
1657
+ color: V.muted,
1658
+ fontSize: 13,
1659
+ lineHeight: 1.55,
1660
+ margin: "0 0 12px"
1661
+ };
1662
+ var hintStyle = {
1663
+ color: V.muted,
1664
+ fontFamily: MONO,
1665
+ fontSize: 10.5,
1666
+ opacity: 0.8
1667
+ };
1668
+ var codeStyle = {
1669
+ fontFamily: MONO,
1670
+ color: V.foreground
1671
+ };
1672
+ var rowStyle = {
1673
+ display: "flex",
1674
+ flexWrap: "wrap",
1675
+ alignItems: "center",
1676
+ justifyContent: "space-between",
1677
+ gap: 12,
1678
+ padding: "1rem"
1679
+ };
1680
+ var rowLabelStyle = {
1681
+ color: V.foreground,
1682
+ fontFamily: MONO,
1683
+ fontSize: 10.5,
1684
+ letterSpacing: "0.12em",
1685
+ textTransform: "uppercase",
1686
+ opacity: 0.8
1687
+ };
1688
+ var inputStyle = {
1689
+ width: "100%",
1690
+ padding: "0.55rem 0.75rem",
1691
+ background: V.background,
1692
+ color: V.foreground,
1693
+ border: `1px solid ${V.border}`,
1694
+ borderRadius: 6,
1695
+ fontFamily: MONO,
1696
+ // 16px — never lower · suppresses iOS Safari focus auto-zoom.
1697
+ fontSize: 16,
1698
+ outline: "none",
1699
+ boxSizing: "border-box"
1700
+ };
1701
+ function btnBase(disabled) {
1702
+ return {
1703
+ padding: "0.5rem 0.875rem",
1704
+ borderRadius: 6,
1705
+ fontFamily: MONO,
1706
+ fontSize: 10.5,
1707
+ letterSpacing: "0.12em",
1708
+ textTransform: "uppercase",
1709
+ cursor: disabled ? "not-allowed" : "pointer",
1710
+ opacity: disabled ? 0.5 : 1
1711
+ };
1712
+ }
1713
+ function primaryBtnStyle(disabled) {
1714
+ return {
1715
+ ...btnBase(disabled),
1716
+ background: V.primary,
1717
+ color: "var(--primary-foreground, #0a0a0a)",
1718
+ border: `1px solid ${V.primary}`,
1719
+ fontWeight: 600
1720
+ };
1721
+ }
1722
+ function ghostBtnStyle(disabled) {
1723
+ return {
1724
+ ...btnBase(disabled),
1725
+ background: V.background,
1726
+ color: V.foreground,
1727
+ border: `1px solid ${V.border}`
1728
+ };
1729
+ }
1730
+ function dangerBtnStyle(disabled) {
1731
+ return {
1732
+ ...btnBase(disabled),
1733
+ background: "transparent",
1734
+ color: V.destructive,
1735
+ border: `1px solid ${V.destructive}`
1736
+ };
1737
+ }
1738
+ var primaryChipStyle = {
1739
+ ...btnBase(false),
1740
+ background: "transparent",
1741
+ color: V.primary,
1742
+ border: `1px solid ${V.primary}`
1743
+ };
773
1744
  function safeReturnTo(input) {
774
1745
  const candidate = input ?? "/";
775
1746
  if (typeof candidate !== "string") return "/";
@@ -785,6 +1756,7 @@ function OcSignIn({
785
1756
  returnTo,
786
1757
  onSuccess,
787
1758
  resolveReturnTo,
1759
+ linkPrompt,
788
1760
  authOrigin = "https://ochk.io",
789
1761
  initialPath = "wallet",
790
1762
  paths,
@@ -793,24 +1765,34 @@ function OcSignIn({
793
1765
  const walletEnabled = paths?.wallet ?? true;
794
1766
  const emailEnabled = paths?.email ?? true;
795
1767
  const safeReturn = safeReturnTo(returnTo);
796
- const [path, setPath] = React4__namespace.useState(initialPath);
797
- const handleSuccess = React4__namespace.useCallback(
798
- async (account, token) => {
799
- if (onSuccess) {
800
- onSuccess(account, token);
801
- return;
802
- }
1768
+ const [path, setPath] = React3__namespace.useState(initialPath);
1769
+ const [signedIn, setSignedIn] = React3__namespace.useState(null);
1770
+ const navigate = React3__namespace.useCallback(
1771
+ async (account) => {
803
1772
  if (resolveReturnTo) {
804
1773
  try {
805
- const target = await resolveReturnTo(account);
806
- hardNavigate(safeReturnTo(target));
1774
+ hardNavigate(safeReturnTo(await resolveReturnTo(account)));
807
1775
  return;
808
1776
  } catch {
809
1777
  }
810
1778
  }
811
1779
  hardNavigate(safeReturn);
812
1780
  },
813
- [onSuccess, resolveReturnTo, safeReturn]
1781
+ [resolveReturnTo, safeReturn]
1782
+ );
1783
+ const handleSuccess = React3__namespace.useCallback(
1784
+ async (account, token) => {
1785
+ if (onSuccess) {
1786
+ onSuccess(account, token);
1787
+ return;
1788
+ }
1789
+ if (linkPrompt) {
1790
+ setSignedIn(account);
1791
+ return;
1792
+ }
1793
+ await navigate(account);
1794
+ },
1795
+ [onSuccess, linkPrompt, navigate]
814
1796
  );
815
1797
  if (!walletEnabled && !emailEnabled) {
816
1798
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -837,6 +1819,15 @@ function OcSignIn({
837
1819
  }
838
1820
  );
839
1821
  }
1822
+ if (signedIn) {
1823
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, "data-oc-signin": "", children: /* @__PURE__ */ jsxRuntime.jsx(
1824
+ LinkPromptStep,
1825
+ {
1826
+ authOrigin,
1827
+ onContinue: () => void navigate(signedIn)
1828
+ }
1829
+ ) });
1830
+ }
840
1831
  const showWallet = walletEnabled && (path === "wallet" || !emailEnabled);
841
1832
  const showEmail = emailEnabled && (path === "email" || !walletEnabled);
842
1833
  const bothEnabled = walletEnabled && emailEnabled;
@@ -879,6 +1870,25 @@ function OcSignIn({
879
1870
  ] })
880
1871
  ] });
881
1872
  }
1873
+ function LinkPromptStep({
1874
+ authOrigin,
1875
+ onContinue
1876
+ }) {
1877
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-oc-signin-linkprompt": "", children: [
1878
+ /* @__PURE__ */ jsxRuntime.jsx(FlowHeader, { label: "\xA7 signed in \xB7 add a backup", children: "You're in. Linking a second way to sign in \u2014 your email or your Bitcoin wallet \u2014 is also your recovery path: lose one, the other still gets you in. Optional; skip with continue." }),
1879
+ /* @__PURE__ */ jsxRuntime.jsx(OcLinkedIdentities, { authOrigin }),
1880
+ /* @__PURE__ */ jsxRuntime.jsx(
1881
+ "button",
1882
+ {
1883
+ type: "button",
1884
+ onClick: onContinue,
1885
+ style: submitStyle(false),
1886
+ "data-oc-signin-continue": "",
1887
+ children: "continue \u2192"
1888
+ }
1889
+ )
1890
+ ] });
1891
+ }
882
1892
  function SigninTab({
883
1893
  active,
884
1894
  onClick,
@@ -910,10 +1920,10 @@ function SigninTab({
910
1920
  );
911
1921
  }
912
1922
  function WalletFlow({ authOrigin, audience, onSuccess }) {
913
- const [address, setAddress] = React4__namespace.useState("");
914
- const [error, setError] = React4__namespace.useState(null);
915
- const [submitting, setSubmitting] = React4__namespace.useState(false);
916
- const [stage, setStage] = React4__namespace.useState("enter");
1923
+ const [address, setAddress] = React3__namespace.useState("");
1924
+ const [error, setError] = React3__namespace.useState(null);
1925
+ const [submitting, setSubmitting] = React3__namespace.useState(false);
1926
+ const [stage, setStage] = React3__namespace.useState("enter");
917
1927
  async function signIn(e) {
918
1928
  e.preventDefault();
919
1929
  const addr = address.trim();
@@ -992,10 +2002,10 @@ function WalletFlow({ authOrigin, audience, onSuccess }) {
992
2002
  spellCheck: false,
993
2003
  required: true,
994
2004
  "aria-invalid": error ? true : void 0,
995
- style: inputStyle
2005
+ style: inputStyle2
996
2006
  }
997
2007
  ),
998
- error && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine, { children: error }),
2008
+ error && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine2, { children: error }),
999
2009
  /* @__PURE__ */ jsxRuntime.jsx(
1000
2010
  "button",
1001
2011
  {
@@ -1009,13 +2019,13 @@ function WalletFlow({ authOrigin, audience, onSuccess }) {
1009
2019
  ] });
1010
2020
  }
1011
2021
  function EmailFlow({ authOrigin, onSuccess }) {
1012
- const [stage, setStage] = React4__namespace.useState("enter");
1013
- const [email, setEmail] = React4__namespace.useState("");
1014
- const [emailError, setEmailError] = React4__namespace.useState(null);
1015
- const [code, setCode] = React4__namespace.useState("");
1016
- const [codeError, setCodeError] = React4__namespace.useState(null);
1017
- const [token, setToken] = React4__namespace.useState(null);
1018
- const [submitting, setSubmitting] = React4__namespace.useState(false);
2022
+ const [stage, setStage] = React3__namespace.useState("enter");
2023
+ const [email, setEmail] = React3__namespace.useState("");
2024
+ const [emailError, setEmailError] = React3__namespace.useState(null);
2025
+ const [code, setCode] = React3__namespace.useState("");
2026
+ const [codeError, setCodeError] = React3__namespace.useState(null);
2027
+ const [token, setToken] = React3__namespace.useState(null);
2028
+ const [submitting, setSubmitting] = React3__namespace.useState(false);
1019
2029
  async function start(e) {
1020
2030
  e.preventDefault();
1021
2031
  const trimmed = email.trim().toLowerCase();
@@ -1091,10 +2101,10 @@ function EmailFlow({ authOrigin, onSuccess }) {
1091
2101
  placeholder: "you@example.com",
1092
2102
  required: true,
1093
2103
  "aria-invalid": emailError ? true : void 0,
1094
- style: inputStyle
2104
+ style: inputStyle2
1095
2105
  }
1096
2106
  ),
1097
- emailError && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine, { children: emailError }),
2107
+ emailError && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine2, { children: emailError }),
1098
2108
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: submitting, style: submitStyle(submitting), children: submitting ? "sending\u2026" : "send one-time code \u2192" }),
1099
2109
  /* @__PURE__ */ jsxRuntime.jsx(Hint, { children: "OTP delivered by the auth host. Codes expire in 10 minutes and are single-use. Rate-limited 5 starts/hour/IP." })
1100
2110
  ] });
@@ -1123,10 +2133,10 @@ function EmailFlow({ authOrigin, onSuccess }) {
1123
2133
  required: true,
1124
2134
  autoFocus: true,
1125
2135
  "aria-invalid": codeError ? true : void 0,
1126
- style: { ...inputStyle, letterSpacing: "0.4em", fontSize: 16 }
2136
+ style: { ...inputStyle2, letterSpacing: "0.4em", fontSize: 16 }
1127
2137
  }
1128
2138
  ),
1129
- codeError && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine, { children: codeError }),
2139
+ codeError && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine2, { children: codeError }),
1130
2140
  /* @__PURE__ */ jsxRuntime.jsx(
1131
2141
  "button",
1132
2142
  {
@@ -1213,7 +2223,7 @@ function Label({ children }) {
1213
2223
  }
1214
2224
  );
1215
2225
  }
1216
- var inputStyle = {
2226
+ var inputStyle2 = {
1217
2227
  width: "100%",
1218
2228
  padding: "0.55rem 0.75rem",
1219
2229
  background: "var(--background, #0a0a0a)",
@@ -1245,7 +2255,7 @@ function submitStyle(disabled) {
1245
2255
  width: "100%"
1246
2256
  };
1247
2257
  }
1248
- function ErrorLine({ children }) {
2258
+ function ErrorLine2({ children }) {
1249
2259
  return /* @__PURE__ */ jsxRuntime.jsx(
1250
2260
  "p",
1251
2261
  {
@@ -1292,13 +2302,13 @@ function isAbortError(err) {
1292
2302
  }
1293
2303
  function useWebAuthnRegister(opts) {
1294
2304
  const authOrigin = resolveAuthOrigin(opts);
1295
- const [status, setStatus] = React4__namespace.useState("idle");
1296
- const [error, setError] = React4__namespace.useState(null);
1297
- const reset = React4__namespace.useCallback(() => {
2305
+ const [status, setStatus] = React3__namespace.useState("idle");
2306
+ const [error, setError] = React3__namespace.useState(null);
2307
+ const reset = React3__namespace.useCallback(() => {
1298
2308
  setStatus("idle");
1299
2309
  setError(null);
1300
2310
  }, []);
1301
- const register = React4__namespace.useCallback(
2311
+ const register = React3__namespace.useCallback(
1302
2312
  async (args) => {
1303
2313
  setError(null);
1304
2314
  setStatus("requesting-options");
@@ -1360,10 +2370,10 @@ function useWebAuthnRegister(opts) {
1360
2370
  function useWebAuthnList(opts) {
1361
2371
  const authOrigin = resolveAuthOrigin(opts);
1362
2372
  const { status: sessionStatus } = useOcSession();
1363
- const [credentials, setCredentials] = React4__namespace.useState([]);
1364
- const [status, setStatus] = React4__namespace.useState("loading");
1365
- const [error, setError] = React4__namespace.useState(null);
1366
- const refetch = React4__namespace.useCallback(async () => {
2373
+ const [credentials, setCredentials] = React3__namespace.useState([]);
2374
+ const [status, setStatus] = React3__namespace.useState("loading");
2375
+ const [error, setError] = React3__namespace.useState(null);
2376
+ const refetch = React3__namespace.useCallback(async () => {
1367
2377
  setError(null);
1368
2378
  if (sessionStatus !== "authenticated") {
1369
2379
  setCredentials([]);
@@ -1391,10 +2401,10 @@ function useWebAuthnList(opts) {
1391
2401
  setStatus("error");
1392
2402
  }
1393
2403
  }, [authOrigin, sessionStatus]);
1394
- React4__namespace.useEffect(() => {
2404
+ React3__namespace.useEffect(() => {
1395
2405
  void refetch();
1396
2406
  }, [refetch]);
1397
- const rename = React4__namespace.useCallback(
2407
+ const rename = React3__namespace.useCallback(
1398
2408
  async (id, label) => {
1399
2409
  try {
1400
2410
  const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
@@ -1419,7 +2429,7 @@ function useWebAuthnList(opts) {
1419
2429
  },
1420
2430
  [authOrigin, refetch]
1421
2431
  );
1422
- const remove = React4__namespace.useCallback(
2432
+ const remove = React3__namespace.useCallback(
1423
2433
  async (id) => {
1424
2434
  try {
1425
2435
  const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
@@ -1447,13 +2457,13 @@ function useWebAuthnList(opts) {
1447
2457
  function useStepUpAuth(opts) {
1448
2458
  const authOrigin = resolveAuthOrigin(opts);
1449
2459
  const { refresh } = useOcSession();
1450
- const [status, setStatus] = React4__namespace.useState("idle");
1451
- const [error, setError] = React4__namespace.useState(null);
1452
- const reset = React4__namespace.useCallback(() => {
2460
+ const [status, setStatus] = React3__namespace.useState("idle");
2461
+ const [error, setError] = React3__namespace.useState(null);
2462
+ const reset = React3__namespace.useCallback(() => {
1453
2463
  setStatus("idle");
1454
2464
  setError(null);
1455
2465
  }, []);
1456
- const stepUp = React4__namespace.useCallback(
2466
+ const stepUp = React3__namespace.useCallback(
1457
2467
  async (args) => {
1458
2468
  setError(null);
1459
2469
  setStatus("requesting-options");
@@ -1542,6 +2552,8 @@ exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
1542
2552
  exports.OcAccountChip = OcAccountChip;
1543
2553
  exports.OcAccountPill = OcAccountPill;
1544
2554
  exports.OcAddressInput = OcAddressInput;
2555
+ exports.OcIdentityBond = OcIdentityBond;
2556
+ exports.OcLinkedIdentities = OcLinkedIdentities;
1545
2557
  exports.OcSessionProvider = OcSessionProvider;
1546
2558
  exports.OcSignIn = OcSignIn;
1547
2559
  exports.OcSignInButton = OcSignInButton;