@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.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import * as React4 from 'react';
2
- import { jsxs, jsx } from 'react/jsx-runtime';
1
+ import * as React3 from 'react';
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
3
  import { startRegistration, startAuthentication } from '@simplewebauthn/browser';
4
4
 
5
5
  // src/provider.tsx
@@ -21,7 +21,7 @@ function buildSignInUrl(cfg, returnTo) {
21
21
  u.searchParams.set("return_to", returnTo);
22
22
  return u.toString();
23
23
  }
24
- var SessionContext = React4.createContext(null);
24
+ var SessionContext = React3.createContext(null);
25
25
  function normalizeAccount(raw) {
26
26
  if (!raw) return null;
27
27
  const didOc = raw.did_oc ?? raw.didOc;
@@ -44,11 +44,11 @@ function OcSessionProvider({
44
44
  config,
45
45
  defaultReturnTo
46
46
  }) {
47
- const cfg = React4.useMemo(() => resolveConfig(config), [config]);
48
- const [account, setAccount] = React4.useState(null);
49
- const [status, setStatus] = React4.useState("loading");
50
- const [error, setError] = React4.useState(null);
51
- const refresh = React4.useCallback(async () => {
47
+ const cfg = React3.useMemo(() => resolveConfig(config), [config]);
48
+ const [account, setAccount] = React3.useState(null);
49
+ const [status, setStatus] = React3.useState("loading");
50
+ const [error, setError] = React3.useState(null);
51
+ const refresh = React3.useCallback(async () => {
52
52
  if (typeof window === "undefined") return;
53
53
  try {
54
54
  const res = await fetch(cfg.mePath, {
@@ -77,10 +77,10 @@ function OcSessionProvider({
77
77
  setError(err instanceof Error ? err : new Error(String(err)));
78
78
  }
79
79
  }, [cfg.mePath]);
80
- React4.useEffect(() => {
80
+ React3.useEffect(() => {
81
81
  void refresh();
82
82
  }, [refresh]);
83
- const signOut = React4.useCallback(async () => {
83
+ const signOut = React3.useCallback(async () => {
84
84
  try {
85
85
  await fetch(`${cfg.authOrigin}${cfg.logoutPath}`, {
86
86
  method: "POST",
@@ -91,7 +91,7 @@ function OcSessionProvider({
91
91
  setAccount(null);
92
92
  setStatus("anonymous");
93
93
  }, [cfg.authOrigin, cfg.logoutPath]);
94
- const value = React4.useMemo(() => {
94
+ const value = React3.useMemo(() => {
95
95
  const returnTo = defaultReturnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
96
96
  return {
97
97
  status,
@@ -105,7 +105,7 @@ function OcSessionProvider({
105
105
  return /* @__PURE__ */ jsx(SessionContext.Provider, { value, children });
106
106
  }
107
107
  function useOcSession() {
108
- const ctx = React4.useContext(SessionContext);
108
+ const ctx = React3.useContext(SessionContext);
109
109
  if (!ctx) {
110
110
  throw new Error(
111
111
  "[@orangecheck/auth-client] useOcSession() must be called inside <OcSessionProvider>"
@@ -114,7 +114,7 @@ function useOcSession() {
114
114
  return ctx;
115
115
  }
116
116
  function useOptionalOcSession() {
117
- return React4.useContext(SessionContext);
117
+ return React3.useContext(SessionContext);
118
118
  }
119
119
  function shortenAddress(addr) {
120
120
  if (addr.length <= 12) return addr;
@@ -129,7 +129,7 @@ function isPrefixOf(value, target) {
129
129
  }
130
130
  var listboxIdCounter = 0;
131
131
  function useUniqueId(prefix) {
132
- const [id] = React4.useState(() => `${prefix}-${++listboxIdCounter}`);
132
+ const [id] = React3.useState(() => `${prefix}-${++listboxIdCounter}`);
133
133
  return id;
134
134
  }
135
135
  function OcAccountChip({
@@ -143,9 +143,9 @@ function OcAccountChip({
143
143
  }) {
144
144
  const { status, account, signInUrl: sessionSignInUrl, signOut } = useOcSession();
145
145
  const signInUrl = signInUrlOverride ?? sessionSignInUrl;
146
- const [open, setOpen] = React4.useState(false);
147
- const wrapRef = React4.useRef(null);
148
- React4.useEffect(() => {
146
+ const [open, setOpen] = React3.useState(false);
147
+ const wrapRef = React3.useRef(null);
148
+ React3.useEffect(() => {
149
149
  if (!open) return;
150
150
  function handleClickOutside(e) {
151
151
  if (wrapRef.current && !wrapRef.current.contains(e.target)) {
@@ -468,9 +468,9 @@ function useOcAddressSuggestion(options) {
468
468
  } = options;
469
469
  const { status, account } = useOcSession();
470
470
  const sessionAddress = status === "authenticated" ? account?.primaryBtc ?? null : null;
471
- const [open, setOpen] = React4.useState(false);
472
- const [highlighted, setHighlighted] = React4.useState(false);
473
- const blurTimer = React4.useRef(null);
471
+ const [open, setOpen] = React3.useState(false);
472
+ const [highlighted, setHighlighted] = React3.useState(false);
473
+ const blurTimer = React3.useRef(null);
474
474
  const listboxId = useUniqueId("oc-addr-listbox");
475
475
  const optionId = `${listboxId}-opt`;
476
476
  const valueMatchesSession = sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();
@@ -482,7 +482,7 @@ function useOcAddressSuggestion(options) {
482
482
  setOpen(false);
483
483
  setHighlighted(false);
484
484
  }
485
- React4.useEffect(() => {
485
+ React3.useEffect(() => {
486
486
  return () => {
487
487
  if (blurTimer.current != null) window.clearTimeout(blurTimer.current);
488
488
  };
@@ -585,7 +585,7 @@ function useOcAddressSuggestion(options) {
585
585
  ) : null;
586
586
  return { inputProps, popover };
587
587
  }
588
- var OcAddressInput = React4.forwardRef(
588
+ var OcAddressInput = React3.forwardRef(
589
589
  function OcAddressInput2({
590
590
  value,
591
591
  onValueChange,
@@ -600,15 +600,15 @@ var OcAddressInput = React4.forwardRef(
600
600
  }, forwardedRef) {
601
601
  const { status, account } = useOcSession();
602
602
  const sessionAddress = status === "authenticated" ? account?.primaryBtc ?? null : null;
603
- const [open, setOpen] = React4.useState(false);
604
- const [highlighted, setHighlighted] = React4.useState(false);
605
- const innerRef = React4.useRef(null);
603
+ const [open, setOpen] = React3.useState(false);
604
+ const [highlighted, setHighlighted] = React3.useState(false);
605
+ const innerRef = React3.useRef(null);
606
606
  const setRef = (node) => {
607
607
  innerRef.current = node;
608
608
  if (typeof forwardedRef === "function") forwardedRef(node);
609
609
  else if (forwardedRef) forwardedRef.current = node;
610
610
  };
611
- const blurTimer = React4.useRef(null);
611
+ const blurTimer = React3.useRef(null);
612
612
  const listboxId = useUniqueId("oc-addr-listbox");
613
613
  const optionId = `${listboxId}-opt`;
614
614
  const valueMatchesSession = sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();
@@ -655,7 +655,7 @@ var OcAddressInput = React4.forwardRef(
655
655
  }
656
656
  onKeyDown?.(e);
657
657
  }
658
- React4.useEffect(() => {
658
+ React3.useEffect(() => {
659
659
  return () => {
660
660
  if (blurTimer.current != null) window.clearTimeout(blurTimer.current);
661
661
  };
@@ -748,6 +748,977 @@ var OcAddressInput = React4.forwardRef(
748
748
  );
749
749
  }
750
750
  );
751
+ var DEFAULT_AUTH_ORIGIN = "https://ochk.io";
752
+ function OcLinkedIdentities({
753
+ authOrigin = DEFAULT_AUTH_ORIGIN,
754
+ className,
755
+ onChange
756
+ }) {
757
+ const [resp, setResp] = React3.useState(null);
758
+ const [error, setError] = React3.useState(null);
759
+ const [showEmailLink, setShowEmailLink] = React3.useState(false);
760
+ const [showBtcLink, setShowBtcLink] = React3.useState(false);
761
+ const refresh = React3.useCallback(async () => {
762
+ setError(null);
763
+ try {
764
+ const r = await fetch(`${authOrigin}/api/auth/identities`, {
765
+ credentials: "include"
766
+ });
767
+ if (!r.ok) {
768
+ if (r.status === 401) {
769
+ setError("not signed in");
770
+ return;
771
+ }
772
+ throw new Error(`http_${r.status}`);
773
+ }
774
+ setResp(await r.json());
775
+ } catch (e) {
776
+ setError(e instanceof Error ? e.message : "load failed");
777
+ }
778
+ }, [authOrigin]);
779
+ React3.useEffect(() => {
780
+ void refresh();
781
+ }, [refresh]);
782
+ const afterChange = React3.useCallback(() => {
783
+ setShowEmailLink(false);
784
+ setShowBtcLink(false);
785
+ void refresh();
786
+ onChange?.();
787
+ }, [refresh, onChange]);
788
+ if (error) {
789
+ return /* @__PURE__ */ jsxs("div", { className, "data-oc-li": "", style: panelStyle("warn"), role: "alert", children: [
790
+ /* @__PURE__ */ jsx(SectionLabel, { tone: "warn", children: "\xA7 identities \xB7 load deferred" }),
791
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
792
+ "Couldn't reach the auth host (",
793
+ error,
794
+ "). Linking is temporarily unavailable; your signed-in identity still works as normal."
795
+ ] }),
796
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void refresh(), style: ghostBtnStyle(false), children: "retry" })
797
+ ] });
798
+ }
799
+ if (!resp) {
800
+ return /* @__PURE__ */ jsx("div", { className, "data-oc-li": "", style: panelStyle(), children: /* @__PURE__ */ jsxs("span", { style: hintStyle, children: [
801
+ "> ",
802
+ "loading\u2026"
803
+ ] }) });
804
+ }
805
+ const linked = resp.linked ?? [];
806
+ const emailRows = linked.filter((i) => i.kind === "email");
807
+ const btcRows = linked.filter((i) => i.kind === "btc");
808
+ const hasEmail = emailRows.some((i) => i.value);
809
+ const hasBtc = btcRows.some((i) => i.value);
810
+ const mergedFrom = resp.merged_from ?? [];
811
+ return /* @__PURE__ */ jsxs("div", { className, "data-oc-li": "", style: { display: "grid", gap: 16 }, children: [
812
+ /* @__PURE__ */ jsxs("div", { style: panelStyle(), children: [
813
+ /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 what this is" }),
814
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
815
+ "Your public identity is the opaque ",
816
+ /* @__PURE__ */ jsx("code", { style: codeStyle, children: "did:oc:" }),
817
+ " 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."
818
+ ] })
819
+ ] }),
820
+ mergedFrom.length > 0 && /* @__PURE__ */ jsx(ConsolidationPanel, { entries: mergedFrom }),
821
+ /* @__PURE__ */ jsxs("div", { style: panelStyle("plain"), "data-oc-li-rows": "", children: [
822
+ /* @__PURE__ */ jsx(
823
+ IdentityGroup,
824
+ {
825
+ kind: "email",
826
+ title: "email",
827
+ rows: emailRows,
828
+ authOrigin,
829
+ onChange: afterChange,
830
+ addCta: hasEmail ? null : "link an email",
831
+ onAdd: () => {
832
+ setShowEmailLink((v) => !v);
833
+ setShowBtcLink(false);
834
+ }
835
+ }
836
+ ),
837
+ /* @__PURE__ */ jsx("div", { style: { borderTop: `1px solid ${V.border}` } }),
838
+ /* @__PURE__ */ jsx(
839
+ IdentityGroup,
840
+ {
841
+ kind: "btc",
842
+ title: "bitcoin address",
843
+ rows: btcRows,
844
+ authOrigin,
845
+ onChange: afterChange,
846
+ addCta: hasBtc ? null : "link a Bitcoin address",
847
+ onAdd: () => {
848
+ setShowBtcLink((v) => !v);
849
+ setShowEmailLink(false);
850
+ }
851
+ }
852
+ )
853
+ ] }),
854
+ showEmailLink && /* @__PURE__ */ jsx(
855
+ EmailLinkForm,
856
+ {
857
+ authOrigin,
858
+ onDone: afterChange,
859
+ onCancel: () => setShowEmailLink(false)
860
+ }
861
+ ),
862
+ showBtcLink && /* @__PURE__ */ jsx(
863
+ BtcLinkForm,
864
+ {
865
+ authOrigin,
866
+ didOc: resp.did_oc ?? null,
867
+ onDone: afterChange,
868
+ onCancel: () => setShowBtcLink(false)
869
+ }
870
+ ),
871
+ hasBtc && resp.did_oc && /* @__PURE__ */ jsx(OcIdentityBond, { didOc: resp.did_oc, btc: btcRows.find((i) => !!i.value).value })
872
+ ] });
873
+ }
874
+ function IdentityGroup({
875
+ kind,
876
+ title,
877
+ rows,
878
+ authOrigin,
879
+ onChange,
880
+ addCta,
881
+ onAdd
882
+ }) {
883
+ if (rows.length === 0) {
884
+ return /* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
885
+ /* @__PURE__ */ jsxs("div", { children: [
886
+ /* @__PURE__ */ jsxs("div", { style: rowLabelStyle, children: [
887
+ "\xA7 ",
888
+ title
889
+ ] }),
890
+ /* @__PURE__ */ jsx("div", { style: { ...bodyStyle, marginTop: 4 }, children: "none linked" })
891
+ ] }),
892
+ addCta && /* @__PURE__ */ jsxs("button", { type: "button", onClick: onAdd, style: primaryChipStyle, children: [
893
+ "+ ",
894
+ addCta
895
+ ] })
896
+ ] });
897
+ }
898
+ return /* @__PURE__ */ jsx(Fragment, { children: rows.map((r) => /* @__PURE__ */ jsx(
899
+ IdentityRow,
900
+ {
901
+ row: r,
902
+ kind,
903
+ authOrigin,
904
+ onChange
905
+ },
906
+ r.id
907
+ )) });
908
+ }
909
+ function IdentityRow({
910
+ row,
911
+ kind,
912
+ authOrigin,
913
+ onChange
914
+ }) {
915
+ const [shown, setShown] = React3.useState(false);
916
+ const [confirming, setConfirming] = React3.useState(false);
917
+ const [busy, setBusy] = React3.useState(false);
918
+ const [err, setErr] = React3.useState(null);
919
+ const masked = row.value ? mask(row.value, kind) : "\u2014 pending recovery \u2014";
920
+ const display = shown && row.value ? row.value : masked;
921
+ async function unlink() {
922
+ if (row.is_primary) return;
923
+ setBusy(true);
924
+ setErr(null);
925
+ try {
926
+ const r = await fetch(
927
+ `${authOrigin}/api/auth/identities?id=${encodeURIComponent(row.id)}`,
928
+ { method: "DELETE", credentials: "include" }
929
+ );
930
+ if (!r.ok) {
931
+ const j = await r.json().catch(() => ({}));
932
+ throw new Error(j.reason ?? `http_${r.status}`);
933
+ }
934
+ onChange();
935
+ } catch (e) {
936
+ setErr(e instanceof Error ? e.message : "unlink failed");
937
+ setBusy(false);
938
+ setConfirming(false);
939
+ }
940
+ }
941
+ return /* @__PURE__ */ jsxs("div", { style: rowStyle, "data-oc-li-row": kind, children: [
942
+ /* @__PURE__ */ jsxs("div", { style: { minWidth: 0, flex: 1 }, children: [
943
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: "4px 12px" }, children: [
944
+ /* @__PURE__ */ jsxs("span", { style: rowLabelStyle, children: [
945
+ "\xA7 ",
946
+ kind === "email" ? "email" : "bitcoin address"
947
+ ] }),
948
+ row.is_primary && /* @__PURE__ */ jsx("span", { style: { ...rowLabelStyle, color: V.primary }, children: "primary \xB7 signup-time" })
949
+ ] }),
950
+ /* @__PURE__ */ jsx(
951
+ "div",
952
+ {
953
+ style: {
954
+ marginTop: 4,
955
+ color: V.foreground,
956
+ fontFamily: MONO,
957
+ fontSize: 12,
958
+ wordBreak: "break-all"
959
+ },
960
+ children: display
961
+ }
962
+ ),
963
+ row.verified_at && /* @__PURE__ */ jsxs("div", { style: { ...hintStyle, marginTop: 4 }, children: [
964
+ "verified \xB7 ",
965
+ new Date(row.verified_at).toUTCString()
966
+ ] }),
967
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err })
968
+ ] }),
969
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexShrink: 0, gap: 8, flexWrap: "wrap" }, children: [
970
+ row.value && /* @__PURE__ */ jsx(
971
+ "button",
972
+ {
973
+ type: "button",
974
+ onClick: () => setShown((s) => !s),
975
+ style: ghostBtnStyle(false),
976
+ children: shown ? "hide" : "show"
977
+ }
978
+ ),
979
+ !row.is_primary && !confirming && /* @__PURE__ */ jsx(
980
+ "button",
981
+ {
982
+ type: "button",
983
+ onClick: () => setConfirming(true),
984
+ style: dangerBtnStyle(false),
985
+ children: "unlink"
986
+ }
987
+ ),
988
+ !row.is_primary && confirming && /* @__PURE__ */ jsxs(Fragment, { children: [
989
+ /* @__PURE__ */ jsx(
990
+ "button",
991
+ {
992
+ type: "button",
993
+ onClick: () => void unlink(),
994
+ disabled: busy,
995
+ style: dangerBtnStyle(busy),
996
+ children: busy ? "unlinking\u2026" : "confirm unlink"
997
+ }
998
+ ),
999
+ /* @__PURE__ */ jsx(
1000
+ "button",
1001
+ {
1002
+ type: "button",
1003
+ onClick: () => setConfirming(false),
1004
+ disabled: busy,
1005
+ style: ghostBtnStyle(busy),
1006
+ children: "cancel"
1007
+ }
1008
+ )
1009
+ ] })
1010
+ ] })
1011
+ ] });
1012
+ }
1013
+ function ConsolidationPanel({ entries }) {
1014
+ return /* @__PURE__ */ jsxs("div", { style: panelStyle("success"), "data-oc-li-consolidated": "", children: [
1015
+ /* @__PURE__ */ jsxs(SectionLabel, { tone: "success", children: [
1016
+ "\xA7 consolidated accounts \xB7 ",
1017
+ entries.length
1018
+ ] }),
1019
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1020
+ entries.length === 1 ? "A previous OC account" : "Previous OC accounts",
1021
+ " you signed up with, folded into this one under dual proof of control. Historical activity is unioned into this account automatically;",
1022
+ " ",
1023
+ entries.length === 1 ? "it" : "they",
1024
+ " can no longer sign in."
1025
+ ] }),
1026
+ /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", margin: "12px 0 0", padding: 0 }, children: entries.map((e) => /* @__PURE__ */ jsxs(
1027
+ "li",
1028
+ {
1029
+ style: {
1030
+ display: "flex",
1031
+ flexWrap: "wrap",
1032
+ gap: 8,
1033
+ padding: "8px 0",
1034
+ borderTop: `1px solid ${V.border}`,
1035
+ fontFamily: MONO,
1036
+ fontSize: 11
1037
+ },
1038
+ children: [
1039
+ /* @__PURE__ */ jsx("code", { style: { color: V.foreground, wordBreak: "break-all" }, children: e.did_oc }),
1040
+ /* @__PURE__ */ jsxs("span", { style: { ...hintStyle, marginLeft: "auto" }, children: [
1041
+ "created ",
1042
+ new Date(e.created_at).toLocaleDateString()
1043
+ ] })
1044
+ ]
1045
+ },
1046
+ e.did_oc
1047
+ )) })
1048
+ ] });
1049
+ }
1050
+ function EmailLinkForm({
1051
+ authOrigin,
1052
+ onDone,
1053
+ onCancel
1054
+ }) {
1055
+ const [step, setStep] = React3.useState("email");
1056
+ const [email, setEmail] = React3.useState("");
1057
+ const [code, setCode] = React3.useState("");
1058
+ const [token, setToken] = React3.useState("");
1059
+ const [busy, setBusy] = React3.useState(false);
1060
+ const [err, setErr] = React3.useState(null);
1061
+ const [transfer, setTransfer] = React3.useState(false);
1062
+ async function start() {
1063
+ setBusy(true);
1064
+ setErr(null);
1065
+ try {
1066
+ const r = await fetch(`${authOrigin}/api/auth/email-otp/start`, {
1067
+ method: "POST",
1068
+ credentials: "include",
1069
+ headers: { "Content-Type": "application/json" },
1070
+ body: JSON.stringify({ email: email.trim().toLowerCase() })
1071
+ });
1072
+ const j = await r.json();
1073
+ if (!r.ok || !j.token) throw new Error(j.reason ?? `http_${r.status}`);
1074
+ setToken(j.token);
1075
+ setStep("code");
1076
+ } catch (e) {
1077
+ setErr(e instanceof Error ? e.message : "could not send code");
1078
+ } finally {
1079
+ setBusy(false);
1080
+ }
1081
+ }
1082
+ async function verify(confirmTransfer) {
1083
+ setBusy(true);
1084
+ setErr(null);
1085
+ try {
1086
+ const r = await fetch(`${authOrigin}/api/auth/link/email`, {
1087
+ method: "POST",
1088
+ credentials: "include",
1089
+ headers: { "Content-Type": "application/json" },
1090
+ body: JSON.stringify({
1091
+ email: email.trim().toLowerCase(),
1092
+ code: code.trim(),
1093
+ token,
1094
+ ...confirmTransfer ? { confirm_transfer: true } : {}
1095
+ })
1096
+ });
1097
+ const j = await r.json();
1098
+ if (r.status === 409 && j.reason === "email_linked_elsewhere") {
1099
+ setTransfer(true);
1100
+ setBusy(false);
1101
+ return;
1102
+ }
1103
+ if (!r.ok || !j.ok) throw new Error(j.reason ?? `http_${r.status}`);
1104
+ onDone();
1105
+ } catch (e) {
1106
+ setErr(e instanceof Error ? e.message : "link failed");
1107
+ setBusy(false);
1108
+ }
1109
+ }
1110
+ return /* @__PURE__ */ jsxs("div", { style: panelStyle("accent"), "data-oc-li-form": "email", children: [
1111
+ /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 link an email" }),
1112
+ step === "email" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1113
+ /* @__PURE__ */ jsx(
1114
+ "input",
1115
+ {
1116
+ type: "email",
1117
+ autoComplete: "email",
1118
+ value: email,
1119
+ onChange: (e) => setEmail(e.target.value),
1120
+ placeholder: "you@example.com",
1121
+ style: inputStyle
1122
+ }
1123
+ ),
1124
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
1125
+ /* @__PURE__ */ jsxs(FormButtons, { children: [
1126
+ /* @__PURE__ */ jsx(
1127
+ "button",
1128
+ {
1129
+ type: "button",
1130
+ onClick: () => void start(),
1131
+ disabled: busy || email.trim().length < 5,
1132
+ style: primaryBtnStyle(busy || email.trim().length < 5),
1133
+ children: busy ? "sending\u2026" : "send code"
1134
+ }
1135
+ ),
1136
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, style: ghostBtnStyle(busy), children: "cancel" })
1137
+ ] })
1138
+ ] }) : transfer ? /* @__PURE__ */ jsx(
1139
+ TransferConfirm,
1140
+ {
1141
+ what: "email",
1142
+ busy,
1143
+ onConfirm: () => void verify(true),
1144
+ onCancel,
1145
+ err
1146
+ }
1147
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1148
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1149
+ "Enter the 6-digit code sent to ",
1150
+ email,
1151
+ "."
1152
+ ] }),
1153
+ /* @__PURE__ */ jsx(
1154
+ "input",
1155
+ {
1156
+ type: "text",
1157
+ inputMode: "numeric",
1158
+ autoComplete: "one-time-code",
1159
+ maxLength: 6,
1160
+ value: code,
1161
+ onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
1162
+ placeholder: "123456",
1163
+ style: { ...inputStyle, letterSpacing: "0.3em" }
1164
+ }
1165
+ ),
1166
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
1167
+ /* @__PURE__ */ jsxs(FormButtons, { children: [
1168
+ /* @__PURE__ */ jsx(
1169
+ "button",
1170
+ {
1171
+ type: "button",
1172
+ onClick: () => void verify(false),
1173
+ disabled: busy || code.length !== 6,
1174
+ style: primaryBtnStyle(busy || code.length !== 6),
1175
+ children: busy ? "linking\u2026" : "verify + link"
1176
+ }
1177
+ ),
1178
+ /* @__PURE__ */ jsx(
1179
+ "button",
1180
+ {
1181
+ type: "button",
1182
+ onClick: () => setStep("email"),
1183
+ disabled: busy,
1184
+ style: ghostBtnStyle(busy),
1185
+ children: "back"
1186
+ }
1187
+ )
1188
+ ] })
1189
+ ] })
1190
+ ] });
1191
+ }
1192
+ function BtcLinkForm({
1193
+ authOrigin,
1194
+ didOc,
1195
+ onDone,
1196
+ onCancel
1197
+ }) {
1198
+ const [step, setStep] = React3.useState("request");
1199
+ const [addr, setAddr] = React3.useState("");
1200
+ const [challenge, setChallenge] = React3.useState("");
1201
+ const [busy, setBusy] = React3.useState(false);
1202
+ const [err, setErr] = React3.useState(null);
1203
+ const [pendingSig, setPendingSig] = React3.useState(null);
1204
+ async function getChallenge() {
1205
+ if (!didOc) {
1206
+ setErr("session not loaded yet \xB7 refresh and retry");
1207
+ return;
1208
+ }
1209
+ setBusy(true);
1210
+ setErr(null);
1211
+ try {
1212
+ const r = await fetch(
1213
+ `${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&purpose=link&audience=${encodeURIComponent(didOc)}`
1214
+ );
1215
+ const j = await r.json();
1216
+ if (!r.ok || !j.message) throw new Error(j.reason ?? `http_${r.status}`);
1217
+ setChallenge(j.message);
1218
+ setStep("sign");
1219
+ } catch (e) {
1220
+ setErr(e instanceof Error ? e.message : "challenge failed");
1221
+ } finally {
1222
+ setBusy(false);
1223
+ }
1224
+ }
1225
+ async function postLink(sig, confirmTransfer) {
1226
+ setBusy(true);
1227
+ setErr(null);
1228
+ try {
1229
+ const r = await fetch(`${authOrigin}/api/auth/link/btc`, {
1230
+ method: "POST",
1231
+ credentials: "include",
1232
+ headers: { "Content-Type": "application/json" },
1233
+ body: JSON.stringify({
1234
+ message: challenge,
1235
+ signature: sig,
1236
+ ...confirmTransfer ? { confirm_transfer: true } : {}
1237
+ })
1238
+ });
1239
+ const j = await r.json();
1240
+ if (r.status === 409 && j.reason === "address_linked_elsewhere") {
1241
+ setPendingSig(sig);
1242
+ setBusy(false);
1243
+ return;
1244
+ }
1245
+ if (!r.ok || !j.ok) throw new Error(j.reason ?? `http_${r.status}`);
1246
+ onDone();
1247
+ } catch (e) {
1248
+ setErr(e instanceof Error ? e.message : "link failed");
1249
+ setBusy(false);
1250
+ }
1251
+ }
1252
+ return /* @__PURE__ */ jsxs("div", { style: panelStyle("accent"), "data-oc-li-form": "btc", children: [
1253
+ /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 link a Bitcoin address" }),
1254
+ step === "request" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1255
+ /* @__PURE__ */ jsx("p", { style: bodyStyle, children: "Enter a Bitcoin address you control. We generate a challenge and your wallet signs it (BIP-322)." }),
1256
+ /* @__PURE__ */ jsx(
1257
+ "input",
1258
+ {
1259
+ type: "text",
1260
+ value: addr,
1261
+ onChange: (e) => setAddr(e.target.value.trim()),
1262
+ placeholder: "bc1q\u2026",
1263
+ autoCapitalize: "none",
1264
+ spellCheck: false,
1265
+ style: { ...inputStyle, fontFamily: MONO }
1266
+ }
1267
+ ),
1268
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
1269
+ /* @__PURE__ */ jsxs(FormButtons, { children: [
1270
+ /* @__PURE__ */ jsx(
1271
+ "button",
1272
+ {
1273
+ type: "button",
1274
+ onClick: () => void getChallenge(),
1275
+ disabled: busy || addr.length < 14,
1276
+ style: primaryBtnStyle(busy || addr.length < 14),
1277
+ children: busy ? "requesting\u2026" : "get challenge"
1278
+ }
1279
+ ),
1280
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, style: ghostBtnStyle(busy), children: "cancel" })
1281
+ ] })
1282
+ ] }) : pendingSig ? /* @__PURE__ */ jsx(
1283
+ TransferConfirm,
1284
+ {
1285
+ what: "bitcoin address",
1286
+ busy,
1287
+ onConfirm: () => void postLink(pendingSig, true),
1288
+ onCancel,
1289
+ err
1290
+ }
1291
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1292
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1293
+ "Sign the challenge with the wallet that controls",
1294
+ " ",
1295
+ /* @__PURE__ */ jsx("span", { style: { fontFamily: MONO, wordBreak: "break-all" }, children: addr }),
1296
+ "."
1297
+ ] }),
1298
+ /* @__PURE__ */ jsx(
1299
+ LazyWalletButton,
1300
+ {
1301
+ address: addr,
1302
+ message: challenge,
1303
+ showManual: true,
1304
+ layout: "list",
1305
+ heading: null,
1306
+ onSigned: (sig) => void postLink(sig, false),
1307
+ onError: (e) => setErr(e?.message ?? "wallet rejected")
1308
+ }
1309
+ ),
1310
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
1311
+ /* @__PURE__ */ jsx(FormButtons, { children: /* @__PURE__ */ jsx(
1312
+ "button",
1313
+ {
1314
+ type: "button",
1315
+ onClick: () => {
1316
+ setStep("request");
1317
+ setErr(null);
1318
+ },
1319
+ disabled: busy,
1320
+ style: ghostBtnStyle(busy),
1321
+ children: "back"
1322
+ }
1323
+ ) })
1324
+ ] })
1325
+ ] });
1326
+ }
1327
+ function TransferConfirm({
1328
+ what,
1329
+ busy,
1330
+ onConfirm,
1331
+ onCancel,
1332
+ err
1333
+ }) {
1334
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1335
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1336
+ "This ",
1337
+ what,
1338
+ " 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?"
1339
+ ] }),
1340
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
1341
+ /* @__PURE__ */ jsxs(FormButtons, { children: [
1342
+ /* @__PURE__ */ jsx(
1343
+ "button",
1344
+ {
1345
+ type: "button",
1346
+ onClick: onConfirm,
1347
+ disabled: busy,
1348
+ style: dangerBtnStyle(busy),
1349
+ children: busy ? "transferring\u2026" : "transfer link here"
1350
+ }
1351
+ ),
1352
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, disabled: busy, style: ghostBtnStyle(busy), children: "cancel" })
1353
+ ] })
1354
+ ] });
1355
+ }
1356
+ function OcIdentityBond({
1357
+ didOc,
1358
+ btc,
1359
+ className
1360
+ }) {
1361
+ const [stage, setStage] = React3.useState("idle");
1362
+ const [message, setMessage] = React3.useState("");
1363
+ const [err, setErr] = React3.useState(null);
1364
+ const [result, setResult] = React3.useState(null);
1365
+ function nip07() {
1366
+ if (typeof window === "undefined") return null;
1367
+ const w = window;
1368
+ return w.nostr ?? null;
1369
+ }
1370
+ async function begin() {
1371
+ setErr(null);
1372
+ const nostr = nip07();
1373
+ if (!nostr) {
1374
+ setErr(
1375
+ "no Nostr signer found \xB7 install a NIP-07 extension (Alby, nos2x) to counter-sign the bond"
1376
+ );
1377
+ return;
1378
+ }
1379
+ try {
1380
+ const sdk = await loadSdk();
1381
+ const pubkeyHex = (await nostr.getPublicKey()).toLowerCase();
1382
+ const npub = sdk.xOnlyHexToNpub(pubkeyHex);
1383
+ const msg = sdk.buildBindingMessage({
1384
+ principal: didOc,
1385
+ btc,
1386
+ nostr: npub,
1387
+ nonce: randomNonceHex(),
1388
+ issued_at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z")
1389
+ });
1390
+ setMessage(msg);
1391
+ setStage("sign-btc");
1392
+ } catch (e) {
1393
+ setErr(e instanceof Error ? e.message : "could not start the bond ceremony");
1394
+ }
1395
+ }
1396
+ async function afterBtcSign(btcSignature) {
1397
+ setErr(null);
1398
+ setStage("sign-nostr");
1399
+ try {
1400
+ const nostr = nip07();
1401
+ if (!nostr) throw new Error("Nostr extension went away \xB7 retry");
1402
+ const sdk = await loadSdk();
1403
+ const pubkeyHex = (await nostr.getPublicKey()).toLowerCase();
1404
+ const template = sdk.buildBindingEventTemplate({
1405
+ message,
1406
+ btcSignature,
1407
+ nostrPubkeyHex: pubkeyHex,
1408
+ btc
1409
+ });
1410
+ const signedEvent = await nostr.signEvent(template);
1411
+ const envelope = sdk.assembleBindingEnvelope({
1412
+ message,
1413
+ btcSignature,
1414
+ btcScheme: "bip322",
1415
+ nostrEvent: signedEvent
1416
+ });
1417
+ const check = sdk.verifyBinding(envelope);
1418
+ if (!check.valid) {
1419
+ throw new Error(`assembled bond failed local verification (${check.status})`);
1420
+ }
1421
+ setStage("publishing");
1422
+ const { publishEvent } = await loadNostrCore();
1423
+ const results = await publishEvent(signedEvent);
1424
+ const ok = results.filter((r) => r.ok).length;
1425
+ if (ok === 0) {
1426
+ throw new Error("no relay accepted the bond \xB7 check your connection and retry");
1427
+ }
1428
+ setResult({
1429
+ bindingId: check.valid ? check.binding_id : sdk.bindingId(message),
1430
+ relays: ok,
1431
+ relaysTotal: results.length
1432
+ });
1433
+ setStage("done");
1434
+ } catch (e) {
1435
+ setErr(e instanceof Error ? e.message : "bond publish failed");
1436
+ setStage("sign-btc");
1437
+ }
1438
+ }
1439
+ return /* @__PURE__ */ jsxs("div", { className, style: panelStyle("accent"), "data-oc-identity-bond": "", children: [
1440
+ /* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 identity bond \xB7 publish to nostr" }),
1441
+ stage === "idle" && /* @__PURE__ */ jsxs(Fragment, { children: [
1442
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1443
+ "Publish a ",
1444
+ /* @__PURE__ */ jsx("strong", { children: "Binding Attestation" }),
1445
+ " \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."
1446
+ ] }),
1447
+ /* @__PURE__ */ jsx(FormButtons, { children: /* @__PURE__ */ jsx(
1448
+ "button",
1449
+ {
1450
+ type: "button",
1451
+ onClick: () => void begin(),
1452
+ style: primaryBtnStyle(false),
1453
+ children: "create & publish bond"
1454
+ }
1455
+ ) })
1456
+ ] }),
1457
+ stage === "sign-btc" && /* @__PURE__ */ jsxs(Fragment, { children: [
1458
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1459
+ "Step 1 of 2 \u2014 sign the binding message with the Bitcoin wallet that controls ",
1460
+ /* @__PURE__ */ jsx("span", { style: { fontFamily: MONO, wordBreak: "break-all" }, children: btc }),
1461
+ "."
1462
+ ] }),
1463
+ /* @__PURE__ */ jsx(
1464
+ LazyWalletButton,
1465
+ {
1466
+ address: btc,
1467
+ message,
1468
+ showManual: true,
1469
+ layout: "list",
1470
+ heading: null,
1471
+ onSigned: (sig) => void afterBtcSign(sig),
1472
+ onError: (e) => setErr(e?.message ?? "wallet rejected the signature")
1473
+ }
1474
+ )
1475
+ ] }),
1476
+ (stage === "sign-nostr" || stage === "publishing") && /* @__PURE__ */ 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" }),
1477
+ stage === "done" && result && /* @__PURE__ */ jsxs(Fragment, { children: [
1478
+ /* @__PURE__ */ jsx(SectionLabel, { tone: "success", children: "\xA7 bond published" }),
1479
+ /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
1480
+ "Your identity bond is live on ",
1481
+ result.relays,
1482
+ "/",
1483
+ result.relaysTotal,
1484
+ " ",
1485
+ "relay",
1486
+ result.relays === 1 ? "" : "s",
1487
+ ". Anyone can now verify the BTC \u21C4 Nostr binding offline."
1488
+ ] }),
1489
+ /* @__PURE__ */ jsxs(
1490
+ "div",
1491
+ {
1492
+ style: {
1493
+ fontFamily: MONO,
1494
+ fontSize: 11,
1495
+ color: V.muted,
1496
+ wordBreak: "break-all"
1497
+ },
1498
+ children: [
1499
+ "binding_id: ",
1500
+ result.bindingId
1501
+ ]
1502
+ }
1503
+ )
1504
+ ] }),
1505
+ err && /* @__PURE__ */ jsx(ErrorLine, { children: err })
1506
+ ] });
1507
+ }
1508
+ function randomNonceHex() {
1509
+ const b = new Uint8Array(16);
1510
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
1511
+ crypto.getRandomValues(b);
1512
+ } else {
1513
+ for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);
1514
+ }
1515
+ let out = "";
1516
+ for (const x of b) out += x.toString(16).padStart(2, "0");
1517
+ return out;
1518
+ }
1519
+ async function loadSdk() {
1520
+ try {
1521
+ return await import('@orangecheck/sdk');
1522
+ } catch {
1523
+ throw new Error("@orangecheck/sdk not installed \xB7 add it to your site to publish bonds");
1524
+ }
1525
+ }
1526
+ async function loadNostrCore() {
1527
+ try {
1528
+ return await import('@orangecheck/nostr-core');
1529
+ } catch {
1530
+ throw new Error(
1531
+ "@orangecheck/nostr-core not installed \xB7 add it to your site to publish bonds"
1532
+ );
1533
+ }
1534
+ }
1535
+ function LazyWalletButton(props) {
1536
+ const [Comp, setComp] = React3.useState(null);
1537
+ const [failed, setFailed] = React3.useState(false);
1538
+ React3.useEffect(() => {
1539
+ let alive = true;
1540
+ import('@orangecheck/wallet-adapter/react').then((m) => {
1541
+ if (alive) {
1542
+ setComp(
1543
+ () => m.OcWalletButton
1544
+ );
1545
+ }
1546
+ }).catch(() => {
1547
+ if (alive) setFailed(true);
1548
+ });
1549
+ return () => {
1550
+ alive = false;
1551
+ };
1552
+ }, []);
1553
+ if (failed) {
1554
+ return /* @__PURE__ */ jsx(ErrorLine, { children: "@orangecheck/wallet-adapter not installed \xB7 add it to your site to link a wallet" });
1555
+ }
1556
+ if (!Comp) return /* @__PURE__ */ jsxs("span", { style: hintStyle, children: [
1557
+ "> ",
1558
+ "loading wallet options\u2026"
1559
+ ] });
1560
+ return /* @__PURE__ */ jsx(Comp, { ...props });
1561
+ }
1562
+ function mask(value, kind) {
1563
+ if (kind === "email") {
1564
+ const [user, host] = value.split("@");
1565
+ if (!user || !host) return "\u2022\u2022\u2022\u2022";
1566
+ const m = user.length <= 2 ? "\u2022".repeat(user.length) : user[0] + "\u2022".repeat(user.length - 2) + user.slice(-1);
1567
+ return `${m}@${host}`;
1568
+ }
1569
+ if (value.length <= 12) return "\u2022".repeat(value.length);
1570
+ return `${value.slice(0, 6)}\u2026${value.slice(-4)}`;
1571
+ }
1572
+ function SectionLabel({
1573
+ children,
1574
+ tone = "primary"
1575
+ }) {
1576
+ const color = tone === "success" ? V.success : tone === "warn" ? V.warn : V.primary;
1577
+ return /* @__PURE__ */ jsx(
1578
+ "div",
1579
+ {
1580
+ style: {
1581
+ color,
1582
+ fontFamily: MONO,
1583
+ fontSize: 10,
1584
+ letterSpacing: "0.18em",
1585
+ textTransform: "uppercase",
1586
+ marginBottom: 8
1587
+ },
1588
+ children
1589
+ }
1590
+ );
1591
+ }
1592
+ function ErrorLine({ children }) {
1593
+ return /* @__PURE__ */ jsx(
1594
+ "p",
1595
+ {
1596
+ role: "alert",
1597
+ style: {
1598
+ margin: "8px 0 0",
1599
+ color: V.destructive,
1600
+ fontFamily: MONO,
1601
+ fontSize: 11
1602
+ },
1603
+ children
1604
+ }
1605
+ );
1606
+ }
1607
+ function FormButtons({ children }) {
1608
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, marginTop: 12 }, children });
1609
+ }
1610
+ var MONO = "ui-monospace, SFMono-Regular, monospace";
1611
+ var V = {
1612
+ primary: "var(--primary, #f97316)",
1613
+ foreground: "var(--foreground, #fafafa)",
1614
+ muted: "var(--muted-foreground, #a1a1aa)",
1615
+ background: "var(--background, #0a0a0a)",
1616
+ card: "var(--card, #111113)",
1617
+ border: "var(--border, #27272a)",
1618
+ destructive: "var(--destructive, #ef4444)",
1619
+ success: "var(--success, #22c55e)",
1620
+ warn: "var(--warning, #eab308)"
1621
+ };
1622
+ function panelStyle(tone = "default") {
1623
+ const base = {
1624
+ background: V.card,
1625
+ border: `1px solid ${V.border}`,
1626
+ padding: "1.25rem"
1627
+ };
1628
+ if (tone === "plain") return { ...base, padding: 0 };
1629
+ if (tone === "accent") return { ...base, borderColor: V.primary, background: "var(--card, #111113)" };
1630
+ if (tone === "success") return { ...base, borderColor: V.success };
1631
+ if (tone === "warn") return { ...base, borderColor: V.warn };
1632
+ return base;
1633
+ }
1634
+ var bodyStyle = {
1635
+ color: V.muted,
1636
+ fontSize: 13,
1637
+ lineHeight: 1.55,
1638
+ margin: "0 0 12px"
1639
+ };
1640
+ var hintStyle = {
1641
+ color: V.muted,
1642
+ fontFamily: MONO,
1643
+ fontSize: 10.5,
1644
+ opacity: 0.8
1645
+ };
1646
+ var codeStyle = {
1647
+ fontFamily: MONO,
1648
+ color: V.foreground
1649
+ };
1650
+ var rowStyle = {
1651
+ display: "flex",
1652
+ flexWrap: "wrap",
1653
+ alignItems: "center",
1654
+ justifyContent: "space-between",
1655
+ gap: 12,
1656
+ padding: "1rem"
1657
+ };
1658
+ var rowLabelStyle = {
1659
+ color: V.foreground,
1660
+ fontFamily: MONO,
1661
+ fontSize: 10.5,
1662
+ letterSpacing: "0.12em",
1663
+ textTransform: "uppercase",
1664
+ opacity: 0.8
1665
+ };
1666
+ var inputStyle = {
1667
+ width: "100%",
1668
+ padding: "0.55rem 0.75rem",
1669
+ background: V.background,
1670
+ color: V.foreground,
1671
+ border: `1px solid ${V.border}`,
1672
+ borderRadius: 6,
1673
+ fontFamily: MONO,
1674
+ // 16px — never lower · suppresses iOS Safari focus auto-zoom.
1675
+ fontSize: 16,
1676
+ outline: "none",
1677
+ boxSizing: "border-box"
1678
+ };
1679
+ function btnBase(disabled) {
1680
+ return {
1681
+ padding: "0.5rem 0.875rem",
1682
+ borderRadius: 6,
1683
+ fontFamily: MONO,
1684
+ fontSize: 10.5,
1685
+ letterSpacing: "0.12em",
1686
+ textTransform: "uppercase",
1687
+ cursor: disabled ? "not-allowed" : "pointer",
1688
+ opacity: disabled ? 0.5 : 1
1689
+ };
1690
+ }
1691
+ function primaryBtnStyle(disabled) {
1692
+ return {
1693
+ ...btnBase(disabled),
1694
+ background: V.primary,
1695
+ color: "var(--primary-foreground, #0a0a0a)",
1696
+ border: `1px solid ${V.primary}`,
1697
+ fontWeight: 600
1698
+ };
1699
+ }
1700
+ function ghostBtnStyle(disabled) {
1701
+ return {
1702
+ ...btnBase(disabled),
1703
+ background: V.background,
1704
+ color: V.foreground,
1705
+ border: `1px solid ${V.border}`
1706
+ };
1707
+ }
1708
+ function dangerBtnStyle(disabled) {
1709
+ return {
1710
+ ...btnBase(disabled),
1711
+ background: "transparent",
1712
+ color: V.destructive,
1713
+ border: `1px solid ${V.destructive}`
1714
+ };
1715
+ }
1716
+ var primaryChipStyle = {
1717
+ ...btnBase(false),
1718
+ background: "transparent",
1719
+ color: V.primary,
1720
+ border: `1px solid ${V.primary}`
1721
+ };
751
1722
  function safeReturnTo(input) {
752
1723
  const candidate = input ?? "/";
753
1724
  if (typeof candidate !== "string") return "/";
@@ -763,6 +1734,7 @@ function OcSignIn({
763
1734
  returnTo,
764
1735
  onSuccess,
765
1736
  resolveReturnTo,
1737
+ linkPrompt,
766
1738
  authOrigin = "https://ochk.io",
767
1739
  initialPath = "wallet",
768
1740
  paths,
@@ -771,24 +1743,34 @@ function OcSignIn({
771
1743
  const walletEnabled = paths?.wallet ?? true;
772
1744
  const emailEnabled = paths?.email ?? true;
773
1745
  const safeReturn = safeReturnTo(returnTo);
774
- const [path, setPath] = React4.useState(initialPath);
775
- const handleSuccess = React4.useCallback(
776
- async (account, token) => {
777
- if (onSuccess) {
778
- onSuccess(account, token);
779
- return;
780
- }
1746
+ const [path, setPath] = React3.useState(initialPath);
1747
+ const [signedIn, setSignedIn] = React3.useState(null);
1748
+ const navigate = React3.useCallback(
1749
+ async (account) => {
781
1750
  if (resolveReturnTo) {
782
1751
  try {
783
- const target = await resolveReturnTo(account);
784
- hardNavigate(safeReturnTo(target));
1752
+ hardNavigate(safeReturnTo(await resolveReturnTo(account)));
785
1753
  return;
786
1754
  } catch {
787
1755
  }
788
1756
  }
789
1757
  hardNavigate(safeReturn);
790
1758
  },
791
- [onSuccess, resolveReturnTo, safeReturn]
1759
+ [resolveReturnTo, safeReturn]
1760
+ );
1761
+ const handleSuccess = React3.useCallback(
1762
+ async (account, token) => {
1763
+ if (onSuccess) {
1764
+ onSuccess(account, token);
1765
+ return;
1766
+ }
1767
+ if (linkPrompt) {
1768
+ setSignedIn(account);
1769
+ return;
1770
+ }
1771
+ await navigate(account);
1772
+ },
1773
+ [onSuccess, linkPrompt, navigate]
792
1774
  );
793
1775
  if (!walletEnabled && !emailEnabled) {
794
1776
  return /* @__PURE__ */ jsxs(
@@ -815,6 +1797,15 @@ function OcSignIn({
815
1797
  }
816
1798
  );
817
1799
  }
1800
+ if (signedIn) {
1801
+ return /* @__PURE__ */ jsx("div", { className, "data-oc-signin": "", children: /* @__PURE__ */ jsx(
1802
+ LinkPromptStep,
1803
+ {
1804
+ authOrigin,
1805
+ onContinue: () => void navigate(signedIn)
1806
+ }
1807
+ ) });
1808
+ }
818
1809
  const showWallet = walletEnabled && (path === "wallet" || !emailEnabled);
819
1810
  const showEmail = emailEnabled && (path === "email" || !walletEnabled);
820
1811
  const bothEnabled = walletEnabled && emailEnabled;
@@ -857,6 +1848,25 @@ function OcSignIn({
857
1848
  ] })
858
1849
  ] });
859
1850
  }
1851
+ function LinkPromptStep({
1852
+ authOrigin,
1853
+ onContinue
1854
+ }) {
1855
+ return /* @__PURE__ */ jsxs("div", { "data-oc-signin-linkprompt": "", children: [
1856
+ /* @__PURE__ */ 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." }),
1857
+ /* @__PURE__ */ jsx(OcLinkedIdentities, { authOrigin }),
1858
+ /* @__PURE__ */ jsx(
1859
+ "button",
1860
+ {
1861
+ type: "button",
1862
+ onClick: onContinue,
1863
+ style: submitStyle(false),
1864
+ "data-oc-signin-continue": "",
1865
+ children: "continue \u2192"
1866
+ }
1867
+ )
1868
+ ] });
1869
+ }
860
1870
  function SigninTab({
861
1871
  active,
862
1872
  onClick,
@@ -888,10 +1898,10 @@ function SigninTab({
888
1898
  );
889
1899
  }
890
1900
  function WalletFlow({ authOrigin, audience, onSuccess }) {
891
- const [address, setAddress] = React4.useState("");
892
- const [error, setError] = React4.useState(null);
893
- const [submitting, setSubmitting] = React4.useState(false);
894
- const [stage, setStage] = React4.useState("enter");
1901
+ const [address, setAddress] = React3.useState("");
1902
+ const [error, setError] = React3.useState(null);
1903
+ const [submitting, setSubmitting] = React3.useState(false);
1904
+ const [stage, setStage] = React3.useState("enter");
895
1905
  async function signIn(e) {
896
1906
  e.preventDefault();
897
1907
  const addr = address.trim();
@@ -970,10 +1980,10 @@ function WalletFlow({ authOrigin, audience, onSuccess }) {
970
1980
  spellCheck: false,
971
1981
  required: true,
972
1982
  "aria-invalid": error ? true : void 0,
973
- style: inputStyle
1983
+ style: inputStyle2
974
1984
  }
975
1985
  ),
976
- error && /* @__PURE__ */ jsx(ErrorLine, { children: error }),
1986
+ error && /* @__PURE__ */ jsx(ErrorLine2, { children: error }),
977
1987
  /* @__PURE__ */ jsx(
978
1988
  "button",
979
1989
  {
@@ -987,13 +1997,13 @@ function WalletFlow({ authOrigin, audience, onSuccess }) {
987
1997
  ] });
988
1998
  }
989
1999
  function EmailFlow({ authOrigin, onSuccess }) {
990
- const [stage, setStage] = React4.useState("enter");
991
- const [email, setEmail] = React4.useState("");
992
- const [emailError, setEmailError] = React4.useState(null);
993
- const [code, setCode] = React4.useState("");
994
- const [codeError, setCodeError] = React4.useState(null);
995
- const [token, setToken] = React4.useState(null);
996
- const [submitting, setSubmitting] = React4.useState(false);
2000
+ const [stage, setStage] = React3.useState("enter");
2001
+ const [email, setEmail] = React3.useState("");
2002
+ const [emailError, setEmailError] = React3.useState(null);
2003
+ const [code, setCode] = React3.useState("");
2004
+ const [codeError, setCodeError] = React3.useState(null);
2005
+ const [token, setToken] = React3.useState(null);
2006
+ const [submitting, setSubmitting] = React3.useState(false);
997
2007
  async function start(e) {
998
2008
  e.preventDefault();
999
2009
  const trimmed = email.trim().toLowerCase();
@@ -1069,10 +2079,10 @@ function EmailFlow({ authOrigin, onSuccess }) {
1069
2079
  placeholder: "you@example.com",
1070
2080
  required: true,
1071
2081
  "aria-invalid": emailError ? true : void 0,
1072
- style: inputStyle
2082
+ style: inputStyle2
1073
2083
  }
1074
2084
  ),
1075
- emailError && /* @__PURE__ */ jsx(ErrorLine, { children: emailError }),
2085
+ emailError && /* @__PURE__ */ jsx(ErrorLine2, { children: emailError }),
1076
2086
  /* @__PURE__ */ jsx("button", { type: "submit", disabled: submitting, style: submitStyle(submitting), children: submitting ? "sending\u2026" : "send one-time code \u2192" }),
1077
2087
  /* @__PURE__ */ jsx(Hint, { children: "OTP delivered by the auth host. Codes expire in 10 minutes and are single-use. Rate-limited 5 starts/hour/IP." })
1078
2088
  ] });
@@ -1101,10 +2111,10 @@ function EmailFlow({ authOrigin, onSuccess }) {
1101
2111
  required: true,
1102
2112
  autoFocus: true,
1103
2113
  "aria-invalid": codeError ? true : void 0,
1104
- style: { ...inputStyle, letterSpacing: "0.4em", fontSize: 16 }
2114
+ style: { ...inputStyle2, letterSpacing: "0.4em", fontSize: 16 }
1105
2115
  }
1106
2116
  ),
1107
- codeError && /* @__PURE__ */ jsx(ErrorLine, { children: codeError }),
2117
+ codeError && /* @__PURE__ */ jsx(ErrorLine2, { children: codeError }),
1108
2118
  /* @__PURE__ */ jsx(
1109
2119
  "button",
1110
2120
  {
@@ -1191,7 +2201,7 @@ function Label({ children }) {
1191
2201
  }
1192
2202
  );
1193
2203
  }
1194
- var inputStyle = {
2204
+ var inputStyle2 = {
1195
2205
  width: "100%",
1196
2206
  padding: "0.55rem 0.75rem",
1197
2207
  background: "var(--background, #0a0a0a)",
@@ -1223,7 +2233,7 @@ function submitStyle(disabled) {
1223
2233
  width: "100%"
1224
2234
  };
1225
2235
  }
1226
- function ErrorLine({ children }) {
2236
+ function ErrorLine2({ children }) {
1227
2237
  return /* @__PURE__ */ jsx(
1228
2238
  "p",
1229
2239
  {
@@ -1270,13 +2280,13 @@ function isAbortError(err) {
1270
2280
  }
1271
2281
  function useWebAuthnRegister(opts) {
1272
2282
  const authOrigin = resolveAuthOrigin(opts);
1273
- const [status, setStatus] = React4.useState("idle");
1274
- const [error, setError] = React4.useState(null);
1275
- const reset = React4.useCallback(() => {
2283
+ const [status, setStatus] = React3.useState("idle");
2284
+ const [error, setError] = React3.useState(null);
2285
+ const reset = React3.useCallback(() => {
1276
2286
  setStatus("idle");
1277
2287
  setError(null);
1278
2288
  }, []);
1279
- const register = React4.useCallback(
2289
+ const register = React3.useCallback(
1280
2290
  async (args) => {
1281
2291
  setError(null);
1282
2292
  setStatus("requesting-options");
@@ -1338,10 +2348,10 @@ function useWebAuthnRegister(opts) {
1338
2348
  function useWebAuthnList(opts) {
1339
2349
  const authOrigin = resolveAuthOrigin(opts);
1340
2350
  const { status: sessionStatus } = useOcSession();
1341
- const [credentials, setCredentials] = React4.useState([]);
1342
- const [status, setStatus] = React4.useState("loading");
1343
- const [error, setError] = React4.useState(null);
1344
- const refetch = React4.useCallback(async () => {
2351
+ const [credentials, setCredentials] = React3.useState([]);
2352
+ const [status, setStatus] = React3.useState("loading");
2353
+ const [error, setError] = React3.useState(null);
2354
+ const refetch = React3.useCallback(async () => {
1345
2355
  setError(null);
1346
2356
  if (sessionStatus !== "authenticated") {
1347
2357
  setCredentials([]);
@@ -1369,10 +2379,10 @@ function useWebAuthnList(opts) {
1369
2379
  setStatus("error");
1370
2380
  }
1371
2381
  }, [authOrigin, sessionStatus]);
1372
- React4.useEffect(() => {
2382
+ React3.useEffect(() => {
1373
2383
  void refetch();
1374
2384
  }, [refetch]);
1375
- const rename = React4.useCallback(
2385
+ const rename = React3.useCallback(
1376
2386
  async (id, label) => {
1377
2387
  try {
1378
2388
  const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
@@ -1397,7 +2407,7 @@ function useWebAuthnList(opts) {
1397
2407
  },
1398
2408
  [authOrigin, refetch]
1399
2409
  );
1400
- const remove = React4.useCallback(
2410
+ const remove = React3.useCallback(
1401
2411
  async (id) => {
1402
2412
  try {
1403
2413
  const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
@@ -1425,13 +2435,13 @@ function useWebAuthnList(opts) {
1425
2435
  function useStepUpAuth(opts) {
1426
2436
  const authOrigin = resolveAuthOrigin(opts);
1427
2437
  const { refresh } = useOcSession();
1428
- const [status, setStatus] = React4.useState("idle");
1429
- const [error, setError] = React4.useState(null);
1430
- const reset = React4.useCallback(() => {
2438
+ const [status, setStatus] = React3.useState("idle");
2439
+ const [error, setError] = React3.useState(null);
2440
+ const reset = React3.useCallback(() => {
1431
2441
  setStatus("idle");
1432
2442
  setError(null);
1433
2443
  }, []);
1434
- const stepUp = React4.useCallback(
2444
+ const stepUp = React3.useCallback(
1435
2445
  async (args) => {
1436
2446
  setError(null);
1437
2447
  setStatus("requesting-options");
@@ -1516,6 +2526,6 @@ function handleSudoRequired(body, args = {}) {
1516
2526
  return false;
1517
2527
  }
1518
2528
 
1519
- export { DEFAULT_CONFIG, OcAccountChip, OcAccountPill, OcAddressInput, OcSessionProvider, OcSignIn, OcSignInButton, buildSignInUrl, handleSudoRequired, redirectToSudo, useOcAddressSuggestion, useOcSession, useOptionalOcSession, useStepUpAuth, useWebAuthnList, useWebAuthnRegister };
2529
+ export { DEFAULT_CONFIG, OcAccountChip, OcAccountPill, OcAddressInput, OcIdentityBond, OcLinkedIdentities, OcSessionProvider, OcSignIn, OcSignInButton, buildSignInUrl, handleSudoRequired, redirectToSudo, useOcAddressSuggestion, useOcSession, useOptionalOcSession, useStepUpAuth, useWebAuthnList, useWebAuthnRegister };
1520
2530
  //# sourceMappingURL=index.mjs.map
1521
2531
  //# sourceMappingURL=index.mjs.map