@orangecheck/auth-client 1.0.0 → 1.1.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
@@ -132,13 +132,15 @@ function useUniqueId(prefix) {
132
132
  }
133
133
  function OcAccountChip({
134
134
  dashboardUrl = "https://ochk.io/dashboard",
135
+ signInUrl: signInUrlOverride,
135
136
  signInLabel = "sign in",
136
137
  className,
137
138
  triggerClassName,
138
139
  popoverClassName,
139
140
  menuItemClassName
140
141
  }) {
141
- const { status, account, signInUrl, signOut } = useOcSession();
142
+ const { status, account, signInUrl: sessionSignInUrl, signOut } = useOcSession();
143
+ const signInUrl = signInUrlOverride ?? sessionSignInUrl;
142
144
  const [open, setOpen] = React2.useState(false);
143
145
  const wrapRef = React2.useRef(null);
144
146
  React2.useEffect(() => {
@@ -395,16 +397,18 @@ function OcSignInButton({
395
397
  label = "sign in with bitcoin",
396
398
  eager = false,
397
399
  className,
400
+ signInUrl: signInUrlOverride,
398
401
  ...rest
399
402
  }) {
400
- const { status, signInUrl } = useOcSession();
403
+ const { status, signInUrl: sessionSignInUrl } = useOcSession();
404
+ const href = signInUrlOverride ?? sessionSignInUrl;
401
405
  if (status === "authenticated") return null;
402
406
  if (!eager && status === "loading") return null;
403
407
  return /* @__PURE__ */ jsx(
404
408
  "a",
405
409
  {
406
410
  ...rest,
407
- href: signInUrl,
411
+ href,
408
412
  className,
409
413
  "data-oc-sign-in-button": "",
410
414
  children: label
@@ -742,7 +746,510 @@ var OcAddressInput = React2.forwardRef(
742
746
  );
743
747
  }
744
748
  );
749
+ function safeReturnTo(input) {
750
+ const candidate = input ?? "/";
751
+ if (typeof candidate !== "string") return "/";
752
+ if (!candidate.startsWith("/") || candidate.startsWith("//")) return "/";
753
+ return candidate;
754
+ }
755
+ function hardNavigate(target) {
756
+ if (typeof window === "undefined") return;
757
+ window.location.assign(target);
758
+ }
759
+ function OcSignIn({
760
+ audience,
761
+ returnTo,
762
+ onSuccess,
763
+ authOrigin = "https://ochk.io",
764
+ initialPath = "wallet",
765
+ paths,
766
+ className
767
+ }) {
768
+ const walletEnabled = paths?.wallet ?? true;
769
+ const emailEnabled = paths?.email ?? true;
770
+ const safeReturn = safeReturnTo(returnTo);
771
+ const [path, setPath] = React2.useState(initialPath);
772
+ const handleSuccess = React2.useCallback(
773
+ (account) => {
774
+ if (onSuccess) {
775
+ onSuccess(account);
776
+ return;
777
+ }
778
+ hardNavigate(safeReturn);
779
+ },
780
+ [onSuccess, safeReturn]
781
+ );
782
+ if (!walletEnabled && !emailEnabled) {
783
+ return /* @__PURE__ */ jsxs(
784
+ "div",
785
+ {
786
+ className,
787
+ "data-oc-signin": "",
788
+ style: {
789
+ padding: "1rem",
790
+ border: "1px solid var(--border, #27272a)",
791
+ color: "var(--foreground, #fafafa)",
792
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
793
+ fontSize: 12
794
+ },
795
+ children: [
796
+ "no signin paths enabled \u2014 set ",
797
+ /* @__PURE__ */ jsx("code", { children: "paths.wallet" }),
798
+ " or",
799
+ " ",
800
+ /* @__PURE__ */ jsx("code", { children: "paths.email" }),
801
+ " to ",
802
+ /* @__PURE__ */ jsx("code", { children: "true" })
803
+ ]
804
+ }
805
+ );
806
+ }
807
+ const showWallet = walletEnabled && (path === "wallet" || !emailEnabled);
808
+ const showEmail = emailEnabled && (path === "email" || !walletEnabled);
809
+ const bothEnabled = walletEnabled && emailEnabled;
810
+ return /* @__PURE__ */ jsxs("div", { className, "data-oc-signin": "", children: [
811
+ bothEnabled && /* @__PURE__ */ jsxs(
812
+ "div",
813
+ {
814
+ role: "tablist",
815
+ "aria-label": "signin method",
816
+ "data-oc-signin-tabs": "",
817
+ style: {
818
+ display: "flex",
819
+ gap: 8,
820
+ marginBottom: 16,
821
+ borderBottom: "1px solid var(--border, #27272a)"
822
+ },
823
+ children: [
824
+ /* @__PURE__ */ jsx(
825
+ SigninTab,
826
+ {
827
+ active: path === "wallet",
828
+ onClick: () => setPath("wallet"),
829
+ children: "bitcoin wallet"
830
+ }
831
+ ),
832
+ /* @__PURE__ */ jsx(SigninTab, { active: path === "email", onClick: () => setPath("email"), children: "email + otp" })
833
+ ]
834
+ }
835
+ ),
836
+ /* @__PURE__ */ jsxs("div", { "data-oc-signin-panel": "", role: "tabpanel", children: [
837
+ showWallet && /* @__PURE__ */ jsx(
838
+ WalletFlow,
839
+ {
840
+ authOrigin,
841
+ audience,
842
+ onSuccess: handleSuccess
843
+ }
844
+ ),
845
+ showEmail && /* @__PURE__ */ jsx(EmailFlow, { authOrigin, onSuccess: handleSuccess })
846
+ ] })
847
+ ] });
848
+ }
849
+ function SigninTab({
850
+ active,
851
+ onClick,
852
+ children
853
+ }) {
854
+ return /* @__PURE__ */ jsx(
855
+ "button",
856
+ {
857
+ type: "button",
858
+ role: "tab",
859
+ "aria-selected": active,
860
+ onClick,
861
+ "data-oc-signin-tab": active ? "active" : "inactive",
862
+ style: {
863
+ padding: "0.6rem 0.875rem",
864
+ background: "transparent",
865
+ border: "none",
866
+ borderBottom: active ? "2px solid var(--primary, #f97316)" : "2px solid transparent",
867
+ color: active ? "var(--foreground, #fafafa)" : "var(--muted-foreground, #a1a1aa)",
868
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
869
+ fontSize: 11,
870
+ letterSpacing: "0.12em",
871
+ textTransform: "uppercase",
872
+ cursor: "pointer",
873
+ marginBottom: -1
874
+ },
875
+ children
876
+ }
877
+ );
878
+ }
879
+ function WalletFlow({ authOrigin, audience, onSuccess }) {
880
+ const [address, setAddress] = React2.useState("");
881
+ const [error, setError] = React2.useState(null);
882
+ const [submitting, setSubmitting] = React2.useState(false);
883
+ const [stage, setStage] = React2.useState("enter");
884
+ async function signIn(e) {
885
+ e.preventDefault();
886
+ const addr = address.trim();
887
+ if (!addr) {
888
+ setError("paste a Bitcoin address");
889
+ return;
890
+ }
891
+ setError(null);
892
+ setSubmitting(true);
893
+ setStage("signing");
894
+ try {
895
+ const challengeRes = await fetch(
896
+ `${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&audience=${encodeURIComponent(audience)}&purpose=login`
897
+ );
898
+ const challenge = await challengeRes.json();
899
+ if (!challengeRes.ok || !challenge.message || !challenge.nonce) {
900
+ throw new Error(challenge.error ?? "challenge_failed");
901
+ }
902
+ let adapter;
903
+ try {
904
+ const moduleId = "@orangecheck/wallet-adapter";
905
+ adapter = await Function("m", "return import(m)")(
906
+ moduleId
907
+ );
908
+ } catch {
909
+ throw new Error(
910
+ "@orangecheck/wallet-adapter not installed \xB7 add it to your consumer site"
911
+ );
912
+ }
913
+ const wallets = adapter.detectWallets().filter((w) => w.detected && w.id !== "manual");
914
+ if (wallets.length === 0) {
915
+ throw new Error("no BIP-322 wallet extension detected \xB7 install one and refresh");
916
+ }
917
+ const wallet = wallets[0];
918
+ const signer = adapter.getSigner(wallet.id, { address: addr });
919
+ const signature = await signer(challenge.message);
920
+ const res = await fetch(`${authOrigin}/api/auth/signin`, {
921
+ method: "POST",
922
+ credentials: "include",
923
+ headers: { "Content-Type": "application/json" },
924
+ body: JSON.stringify({
925
+ message: challenge.message,
926
+ signature,
927
+ scheme: "bip322",
928
+ expectedNonce: challenge.nonce,
929
+ expectedAudience: audience,
930
+ expectedPurpose: "login"
931
+ })
932
+ });
933
+ const json = await res.json();
934
+ if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
935
+ throw new Error(
936
+ "reason" in json && json.reason || "error" in json && json.error || `verify_failed_${res.status}`
937
+ );
938
+ }
939
+ onSuccess(json.account);
940
+ } catch (err) {
941
+ const msg = err instanceof Error ? err.message : "sign-in failed";
942
+ setError(msg);
943
+ setSubmitting(false);
944
+ setStage("enter");
945
+ }
946
+ }
947
+ return /* @__PURE__ */ jsxs("form", { onSubmit: signIn, "data-oc-signin-wallet": "", children: [
948
+ /* @__PURE__ */ jsx(FlowHeader, { label: "\xA7 bitcoin wallet", children: "Sign in with any BIP-322-capable Bitcoin wallet (Sparrow, Xverse, Leather, UniSat, Alby, OKX, Phantom). Paste your address, click sign \u2014 your wallet extension prompts for a one-time signature on a short challenge. The address becomes your OC identity. Private key material never leaves your wallet." }),
949
+ /* @__PURE__ */ jsx(Label, { children: "bitcoin address" }),
950
+ /* @__PURE__ */ jsx(
951
+ "input",
952
+ {
953
+ type: "text",
954
+ value: address,
955
+ onChange: (e) => {
956
+ setAddress(e.target.value);
957
+ if (error) setError(null);
958
+ },
959
+ placeholder: "bc1q\u2026 \xB7 3\u2026 \xB7 1\u2026",
960
+ autoComplete: "off",
961
+ autoCapitalize: "none",
962
+ spellCheck: false,
963
+ required: true,
964
+ "aria-invalid": error ? true : void 0,
965
+ style: inputStyle
966
+ }
967
+ ),
968
+ error && /* @__PURE__ */ jsx(ErrorLine, { children: error }),
969
+ /* @__PURE__ */ jsx(
970
+ "button",
971
+ {
972
+ type: "submit",
973
+ disabled: submitting || !address.trim(),
974
+ style: submitStyle(submitting || !address.trim()),
975
+ children: stage === "signing" && submitting ? "waiting for wallet\u2026" : "sign challenge \xB7 sign me in \u2192"
976
+ }
977
+ ),
978
+ /* @__PURE__ */ jsx(Hint, { children: "Detection picks the first installed BIP-322-capable extension. Address is the one your wallet will sign for." })
979
+ ] });
980
+ }
981
+ function EmailFlow({ authOrigin, onSuccess }) {
982
+ const [stage, setStage] = React2.useState("enter");
983
+ const [email, setEmail] = React2.useState("");
984
+ const [emailError, setEmailError] = React2.useState(null);
985
+ const [code, setCode] = React2.useState("");
986
+ const [codeError, setCodeError] = React2.useState(null);
987
+ const [token, setToken] = React2.useState(null);
988
+ const [submitting, setSubmitting] = React2.useState(false);
989
+ async function start(e) {
990
+ e.preventDefault();
991
+ const trimmed = email.trim().toLowerCase();
992
+ if (!trimmed || !/.+@.+\..+/.test(trimmed)) {
993
+ setEmailError("enter a valid email");
994
+ return;
995
+ }
996
+ setEmailError(null);
997
+ setSubmitting(true);
998
+ try {
999
+ const res = await fetch(`${authOrigin}/api/auth/email-otp/start`, {
1000
+ method: "POST",
1001
+ credentials: "include",
1002
+ headers: { "Content-Type": "application/json" },
1003
+ body: JSON.stringify({ email: trimmed })
1004
+ });
1005
+ const json = await res.json();
1006
+ if (!res.ok || !json.token) {
1007
+ throw new Error(json.reason ?? `start failed (${res.status})`);
1008
+ }
1009
+ setToken(json.token);
1010
+ setEmail(trimmed);
1011
+ setStage("code");
1012
+ } catch (err) {
1013
+ setEmailError(err instanceof Error ? err.message : "failed to send code");
1014
+ } finally {
1015
+ setSubmitting(false);
1016
+ }
1017
+ }
1018
+ async function verify(e) {
1019
+ e.preventDefault();
1020
+ if (!token) return;
1021
+ if (code.length !== 6) {
1022
+ setCodeError("6 digits");
1023
+ return;
1024
+ }
1025
+ setCodeError(null);
1026
+ setSubmitting(true);
1027
+ try {
1028
+ const res = await fetch(`${authOrigin}/api/auth/email-otp/verify`, {
1029
+ method: "POST",
1030
+ credentials: "include",
1031
+ headers: { "Content-Type": "application/json" },
1032
+ body: JSON.stringify({ email, code, token })
1033
+ });
1034
+ const json = await res.json();
1035
+ if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
1036
+ throw new Error(
1037
+ "reason" in json && json.reason || "error" in json && json.error || `verify failed (${res.status})`
1038
+ );
1039
+ }
1040
+ onSuccess(json.account);
1041
+ } catch (err) {
1042
+ setCodeError(err instanceof Error ? err.message : "verify failed");
1043
+ } finally {
1044
+ setSubmitting(false);
1045
+ }
1046
+ }
1047
+ if (stage === "enter") {
1048
+ return /* @__PURE__ */ jsxs("form", { onSubmit: start, "data-oc-signin-email": "", children: [
1049
+ /* @__PURE__ */ jsx(FlowHeader, { label: "\xA7 email + otp", children: "We email you a 6-digit code. No password. The federation provisions a custodial wallet for you in your browser. Best path if you don't have a Bitcoin wallet yet \u2014 graduate to self-custody anytime by adding your BTC address from your account dashboard." }),
1050
+ /* @__PURE__ */ jsx(Label, { children: "email" }),
1051
+ /* @__PURE__ */ jsx(
1052
+ "input",
1053
+ {
1054
+ type: "email",
1055
+ autoComplete: "email",
1056
+ value: email,
1057
+ onChange: (e) => {
1058
+ setEmail(e.target.value);
1059
+ if (emailError) setEmailError(null);
1060
+ },
1061
+ placeholder: "you@example.com",
1062
+ required: true,
1063
+ "aria-invalid": emailError ? true : void 0,
1064
+ style: inputStyle
1065
+ }
1066
+ ),
1067
+ emailError && /* @__PURE__ */ jsx(ErrorLine, { children: emailError }),
1068
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: submitting, style: submitStyle(submitting), children: submitting ? "sending\u2026" : "send one-time code \u2192" }),
1069
+ /* @__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." })
1070
+ ] });
1071
+ }
1072
+ return /* @__PURE__ */ jsxs("form", { onSubmit: verify, "data-oc-signin-email-verify": "", children: [
1073
+ /* @__PURE__ */ jsxs(FlowHeader, { label: "\xA7 enter the code", children: [
1074
+ "Sent a 6-digit code to ",
1075
+ /* @__PURE__ */ jsx("span", { style: { color: "var(--foreground, #fafafa)" }, children: email }),
1076
+ ". Code expires in 10 minutes."
1077
+ ] }),
1078
+ /* @__PURE__ */ jsx(Label, { children: "one-time code" }),
1079
+ /* @__PURE__ */ jsx(
1080
+ "input",
1081
+ {
1082
+ type: "text",
1083
+ inputMode: "numeric",
1084
+ pattern: "\\d{6}",
1085
+ maxLength: 6,
1086
+ autoComplete: "one-time-code",
1087
+ value: code,
1088
+ onChange: (e) => {
1089
+ setCode(e.target.value.replace(/\D/g, "").slice(0, 6));
1090
+ if (codeError) setCodeError(null);
1091
+ },
1092
+ placeholder: "6 digits",
1093
+ required: true,
1094
+ autoFocus: true,
1095
+ "aria-invalid": codeError ? true : void 0,
1096
+ style: { ...inputStyle, letterSpacing: "0.4em", fontSize: 16 }
1097
+ }
1098
+ ),
1099
+ codeError && /* @__PURE__ */ jsx(ErrorLine, { children: codeError }),
1100
+ /* @__PURE__ */ jsx(
1101
+ "button",
1102
+ {
1103
+ type: "submit",
1104
+ disabled: submitting || code.length !== 6,
1105
+ style: submitStyle(submitting || code.length !== 6),
1106
+ children: submitting ? "verifying\u2026" : "verify \xB7 sign me in \u2192"
1107
+ }
1108
+ ),
1109
+ /* @__PURE__ */ jsx(
1110
+ "button",
1111
+ {
1112
+ type: "button",
1113
+ onClick: () => {
1114
+ setStage("enter");
1115
+ setCode("");
1116
+ setToken(null);
1117
+ },
1118
+ style: {
1119
+ marginTop: 12,
1120
+ background: "transparent",
1121
+ border: "none",
1122
+ color: "var(--muted-foreground, #a1a1aa)",
1123
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
1124
+ fontSize: 11,
1125
+ letterSpacing: "0.12em",
1126
+ textTransform: "uppercase",
1127
+ cursor: "pointer",
1128
+ padding: 0
1129
+ },
1130
+ children: "use a different email"
1131
+ }
1132
+ ),
1133
+ /* @__PURE__ */ jsx(Hint, { children: "On verify, oc_session is set on Domain=.ochk.io family-wide." })
1134
+ ] });
1135
+ }
1136
+ function FlowHeader({
1137
+ label,
1138
+ children
1139
+ }) {
1140
+ return /* @__PURE__ */ jsxs("div", { style: { marginBottom: 16 }, children: [
1141
+ /* @__PURE__ */ jsx(
1142
+ "div",
1143
+ {
1144
+ style: {
1145
+ color: "var(--primary, #f97316)",
1146
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
1147
+ fontSize: 10,
1148
+ letterSpacing: "0.18em",
1149
+ textTransform: "uppercase",
1150
+ marginBottom: 6
1151
+ },
1152
+ children: label
1153
+ }
1154
+ ),
1155
+ /* @__PURE__ */ jsx(
1156
+ "p",
1157
+ {
1158
+ style: {
1159
+ color: "var(--muted-foreground, #a1a1aa)",
1160
+ fontSize: 13,
1161
+ lineHeight: 1.55,
1162
+ margin: 0
1163
+ },
1164
+ children
1165
+ }
1166
+ )
1167
+ ] });
1168
+ }
1169
+ function Label({ children }) {
1170
+ return /* @__PURE__ */ jsx(
1171
+ "label",
1172
+ {
1173
+ style: {
1174
+ display: "block",
1175
+ color: "var(--foreground, #fafafa)",
1176
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
1177
+ fontSize: 11,
1178
+ letterSpacing: "0.12em",
1179
+ textTransform: "uppercase",
1180
+ marginBottom: 6
1181
+ },
1182
+ children
1183
+ }
1184
+ );
1185
+ }
1186
+ var inputStyle = {
1187
+ width: "100%",
1188
+ padding: "0.55rem 0.75rem",
1189
+ background: "var(--background, #0a0a0a)",
1190
+ color: "var(--foreground, #fafafa)",
1191
+ border: "1px solid var(--input, #27272a)",
1192
+ borderRadius: 6,
1193
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
1194
+ fontSize: 13,
1195
+ outline: "none"
1196
+ };
1197
+ function submitStyle(disabled) {
1198
+ return {
1199
+ marginTop: 12,
1200
+ padding: "0.6rem 1rem",
1201
+ background: "var(--primary, #f97316)",
1202
+ color: "var(--primary-foreground, #0a0a0a)",
1203
+ border: "none",
1204
+ borderRadius: 6,
1205
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
1206
+ fontSize: 11,
1207
+ letterSpacing: "0.14em",
1208
+ textTransform: "uppercase",
1209
+ fontWeight: 600,
1210
+ cursor: disabled ? "not-allowed" : "pointer",
1211
+ opacity: disabled ? 0.5 : 1,
1212
+ width: "100%"
1213
+ };
1214
+ }
1215
+ function ErrorLine({ children }) {
1216
+ return /* @__PURE__ */ jsx(
1217
+ "p",
1218
+ {
1219
+ role: "alert",
1220
+ style: {
1221
+ marginTop: 6,
1222
+ marginBottom: 0,
1223
+ color: "var(--destructive, #ef4444)",
1224
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
1225
+ fontSize: 11,
1226
+ letterSpacing: "0.04em"
1227
+ },
1228
+ children
1229
+ }
1230
+ );
1231
+ }
1232
+ function Hint({ children }) {
1233
+ return /* @__PURE__ */ jsxs(
1234
+ "p",
1235
+ {
1236
+ style: {
1237
+ marginTop: 12,
1238
+ marginBottom: 0,
1239
+ color: "var(--muted-foreground, #a1a1aa)",
1240
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
1241
+ fontSize: 10.5,
1242
+ lineHeight: 1.55,
1243
+ opacity: 0.7
1244
+ },
1245
+ children: [
1246
+ "> ",
1247
+ children
1248
+ ]
1249
+ }
1250
+ );
1251
+ }
745
1252
 
746
- export { DEFAULT_CONFIG, OcAccountChip, OcAccountPill, OcAddressInput, OcSessionProvider, OcSignInButton, buildSignInUrl, useOcAddressSuggestion, useOcSession, useOptionalOcSession };
1253
+ export { DEFAULT_CONFIG, OcAccountChip, OcAccountPill, OcAddressInput, OcSessionProvider, OcSignIn, OcSignInButton, buildSignInUrl, useOcAddressSuggestion, useOcSession, useOptionalOcSession };
747
1254
  //# sourceMappingURL=index.mjs.map
748
1255
  //# sourceMappingURL=index.mjs.map