@orangecheck/auth-client 2.16.0 → 2.17.1

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
@@ -22,6 +22,11 @@ function buildSignInUrl(cfg, returnTo) {
22
22
  u.searchParams.set("return_to", returnTo);
23
23
  return u.toString();
24
24
  }
25
+ function buildAddAccountUrl(cfg, returnTo) {
26
+ const u = new URL(buildSignInUrl(cfg, returnTo));
27
+ u.searchParams.set("add", "1");
28
+ return u.toString();
29
+ }
25
30
  var SessionContext = React3.createContext(null);
26
31
  function normalizeDisplayIdentity(raw, didOc) {
27
32
  const di = raw.display_identity ?? raw.displayIdentity;
@@ -30,6 +35,19 @@ function normalizeDisplayIdentity(raw, didOc) {
30
35
  }
31
36
  return { kind: "did", value: didOc };
32
37
  }
38
+ function normalizeRosterEntry(raw) {
39
+ const didOc = raw.did_oc ?? raw.didOc;
40
+ if (!didOc) return null;
41
+ const di = raw.display_identity ?? raw.displayIdentity;
42
+ const displayIdentity = di && typeof di === "object" && typeof di.value === "string" && di.value.length > 0 && typeof di.kind === "string" && DISPLAY_IDENTITY_KINDS.includes(di.kind) ? { kind: di.kind, value: di.value } : { kind: "did", value: didOc };
43
+ return {
44
+ didOc,
45
+ displayName: raw.display_name ?? raw.displayName ?? null,
46
+ primaryBtc: raw.primary_btc ?? raw.primaryBtc ?? null,
47
+ displayIdentity,
48
+ lastSeenAt: raw.last_seen_at ?? raw.lastSeenAt ?? null
49
+ };
50
+ }
33
51
  function normalizeAccount(raw) {
34
52
  if (!raw) return null;
35
53
  const didOc = raw.did_oc ?? raw.didOc;
@@ -55,6 +73,7 @@ function OcSessionProvider({
55
73
  }) {
56
74
  const cfg = React3.useMemo(() => resolveConfig(config), [config]);
57
75
  const [account, setAccount] = React3.useState(null);
76
+ const [roster, setRoster] = React3.useState([]);
58
77
  const [status, setStatus] = React3.useState("loading");
59
78
  const [error, setError] = React3.useState(null);
60
79
  const refresh = React3.useCallback(async () => {
@@ -67,6 +86,7 @@ function OcSessionProvider({
67
86
  });
68
87
  if (res.status === 401) {
69
88
  setAccount(null);
89
+ setRoster([]);
70
90
  setStatus("anonymous");
71
91
  setError(null);
72
92
  return;
@@ -78,7 +98,9 @@ function OcSessionProvider({
78
98
  }
79
99
  const body = await res.json();
80
100
  const acct = normalizeAccount(body.account);
101
+ const rosterEntries = Array.isArray(body.roster) ? body.roster.map(normalizeRosterEntry).filter((r) => r !== null) : [];
81
102
  setAccount(acct);
103
+ setRoster(rosterEntries);
82
104
  setStatus(acct ? "authenticated" : "anonymous");
83
105
  setError(null);
84
106
  } catch (err) {
@@ -89,23 +111,63 @@ function OcSessionProvider({
89
111
  React3.useEffect(() => {
90
112
  void refresh();
91
113
  }, [refresh]);
92
- const signOut = React3.useCallback(async () => {
93
- try {
94
- await fetch(`${cfg.authOrigin}${cfg.logoutPath}`, {
114
+ const signOut = React3.useCallback(
115
+ async (opts) => {
116
+ const scope = opts?.scope ?? "all";
117
+ try {
118
+ const url = new URL(`${cfg.authOrigin}${cfg.logoutPath}`);
119
+ if (scope === "current") url.searchParams.set("scope", "current");
120
+ const res = await fetch(url.toString(), {
121
+ method: "POST",
122
+ credentials: "include",
123
+ // `keepalive` lets the logout round-trip complete even if
124
+ // the caller hard-navigates away in the same tick (e.g.
125
+ // `<OcAccountMenu>` redirects home immediately on sign-out).
126
+ // Without it the in-flight request is cancelled on unload
127
+ // and the `.ochk.io` cookie may never get cleared.
128
+ keepalive: true
129
+ });
130
+ if (scope === "current" && res.ok) {
131
+ await refresh();
132
+ return;
133
+ }
134
+ } catch {
135
+ }
136
+ setAccount(null);
137
+ setRoster([]);
138
+ setStatus("anonymous");
139
+ },
140
+ [cfg.authOrigin, cfg.logoutPath, refresh]
141
+ );
142
+ const switchAccount = React3.useCallback(
143
+ async (didOc) => {
144
+ if (typeof window === "undefined") return;
145
+ const res = await fetch(`${cfg.authOrigin}/api/auth/switch`, {
95
146
  method: "POST",
96
147
  credentials: "include",
97
- // `keepalive` lets the logout round-trip complete even if
98
- // the caller hard-navigates away in the same tick (e.g.
99
- // `<OcAccountMenu>` redirects home immediately on sign-out).
100
- // Without it the in-flight request is cancelled on unload
101
- // and the `.ochk.io` cookie may never get cleared.
102
- keepalive: true
148
+ headers: { "Content-Type": "application/json" },
149
+ body: JSON.stringify({ did_oc: didOc })
103
150
  });
104
- } catch {
105
- }
106
- setAccount(null);
107
- setStatus("anonymous");
108
- }, [cfg.authOrigin, cfg.logoutPath]);
151
+ if (!res.ok) {
152
+ let reason = `http_${res.status}`;
153
+ try {
154
+ const body = await res.json();
155
+ if (body.reason) reason = body.reason;
156
+ } catch {
157
+ }
158
+ throw new Error(`[@orangecheck/auth-client] switchAccount failed: ${reason}`);
159
+ }
160
+ await refresh();
161
+ },
162
+ [cfg.authOrigin, refresh]
163
+ );
164
+ const addAccountUrl = React3.useCallback(
165
+ (returnTo) => {
166
+ const rt = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
167
+ return buildAddAccountUrl(cfg, rt);
168
+ },
169
+ [cfg]
170
+ );
109
171
  const setDisplayIdentity = React3.useCallback(
110
172
  async (kind) => {
111
173
  if (typeof window === "undefined") return;
@@ -133,13 +195,28 @@ function OcSessionProvider({
133
195
  return {
134
196
  status,
135
197
  account,
198
+ roster,
136
199
  error,
137
200
  refresh,
138
201
  signOut,
202
+ switchAccount,
203
+ addAccountUrl,
139
204
  setDisplayIdentity,
140
205
  signInUrl: buildSignInUrl(cfg, returnTo)
141
206
  };
142
- }, [status, account, error, refresh, signOut, setDisplayIdentity, cfg, defaultReturnTo]);
207
+ }, [
208
+ status,
209
+ account,
210
+ roster,
211
+ error,
212
+ refresh,
213
+ signOut,
214
+ switchAccount,
215
+ addAccountUrl,
216
+ setDisplayIdentity,
217
+ cfg,
218
+ defaultReturnTo
219
+ ]);
143
220
  return /* @__PURE__ */ jsx(SessionContext.Provider, { value, children });
144
221
  }
145
222
  function useOcSession() {
@@ -1666,6 +1743,7 @@ function OcSignIn({
1666
1743
  onSuccess,
1667
1744
  resolveReturnTo,
1668
1745
  linkPrompt = true,
1746
+ add: addProp,
1669
1747
  authOrigin = "https://ochk.io",
1670
1748
  initialPath = "wallet",
1671
1749
  paths,
@@ -1674,6 +1752,15 @@ function OcSignIn({
1674
1752
  const walletEnabled = paths?.wallet ?? true;
1675
1753
  const emailEnabled = paths?.email ?? true;
1676
1754
  const safeReturn = safeReturnTo(returnTo);
1755
+ const [addMode, setAddMode] = React3.useState(Boolean(addProp));
1756
+ React3.useEffect(() => {
1757
+ if (typeof window === "undefined") return;
1758
+ if (addProp !== void 0) {
1759
+ setAddMode(Boolean(addProp));
1760
+ return;
1761
+ }
1762
+ setAddMode(new URLSearchParams(window.location.search).get("add") === "1");
1763
+ }, [addProp]);
1677
1764
  const [path, setPath] = React3.useState(initialPath);
1678
1765
  const [signedIn, setSignedIn] = React3.useState(null);
1679
1766
  const [linkAlso, setLinkAlso] = React3.useState(false);
@@ -1785,6 +1872,41 @@ function OcSignIn({
1785
1872
  children: "That sign-in didn't complete. Please try again."
1786
1873
  }
1787
1874
  ),
1875
+ addMode && /* @__PURE__ */ jsxs(
1876
+ "div",
1877
+ {
1878
+ "data-oc-signin-add-mode": "",
1879
+ style: {
1880
+ marginBottom: 14,
1881
+ padding: "0.6rem 0.75rem",
1882
+ border: "1px solid var(--primary, #f97316)",
1883
+ borderLeftWidth: 3,
1884
+ borderRadius: 4,
1885
+ background: "color-mix(in srgb, var(--primary, #f97316) 8%, transparent)",
1886
+ color: "var(--foreground, #fafafa)",
1887
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
1888
+ fontSize: 11,
1889
+ lineHeight: 1.6
1890
+ },
1891
+ children: [
1892
+ /* @__PURE__ */ jsx(
1893
+ "strong",
1894
+ {
1895
+ style: {
1896
+ color: "var(--primary, #f97316)",
1897
+ letterSpacing: "0.12em",
1898
+ textTransform: "uppercase",
1899
+ fontSize: 10,
1900
+ display: "block",
1901
+ marginBottom: 3
1902
+ },
1903
+ children: "\xA7 adding another account"
1904
+ }
1905
+ ),
1906
+ "Your current OrangeCheck account stays signed in. The new account joins your browser's roster; you can switch between them anytime from the account menu."
1907
+ ]
1908
+ }
1909
+ ),
1788
1910
  bothEnabled && /* @__PURE__ */ jsxs(
1789
1911
  "div",
1790
1912
  {
@@ -1816,6 +1938,7 @@ function OcSignIn({
1816
1938
  {
1817
1939
  authOrigin,
1818
1940
  audience,
1941
+ add: addMode,
1819
1942
  onSuccess: (a, t) => void handleSuccess(a, t, "wallet")
1820
1943
  }
1821
1944
  ),
@@ -1823,6 +1946,7 @@ function OcSignIn({
1823
1946
  EmailFlow,
1824
1947
  {
1825
1948
  authOrigin,
1949
+ add: addMode,
1826
1950
  onSuccess: (a, t) => void handleSuccess(a, t, "email")
1827
1951
  }
1828
1952
  )
@@ -1970,7 +2094,7 @@ function ProviderSignIn({
1970
2094
  ))
1971
2095
  ] });
1972
2096
  }
1973
- function WalletFlow({ authOrigin, audience, onSuccess }) {
2097
+ function WalletFlow({ authOrigin, audience, add, onSuccess }) {
1974
2098
  const [address, setAddress] = React3.useState("");
1975
2099
  const [error, setError] = React3.useState(null);
1976
2100
  const [submitting, setSubmitting] = React3.useState(false);
@@ -2018,7 +2142,12 @@ function WalletFlow({ authOrigin, audience, onSuccess }) {
2018
2142
  scheme: "bip322",
2019
2143
  expectedNonce: challenge.nonce,
2020
2144
  expectedAudience: audience,
2021
- expectedPurpose: "login"
2145
+ expectedPurpose: "login",
2146
+ // Multi-account add-mode · the auth host preserves
2147
+ // the current roster_id when set, instead of minting
2148
+ // a fresh one. Hosts that haven't deployed the
2149
+ // multi-account migration silently ignore the field.
2150
+ ...add ? { add: true } : {}
2022
2151
  })
2023
2152
  });
2024
2153
  const json = await res.json();
@@ -2069,7 +2198,7 @@ function WalletFlow({ authOrigin, audience, onSuccess }) {
2069
2198
  /* @__PURE__ */ jsx(Hint, { children: "Detection picks the first installed BIP-322-capable extension. Address is the one your wallet will sign for." })
2070
2199
  ] });
2071
2200
  }
2072
- function EmailFlow({ authOrigin, onSuccess }) {
2201
+ function EmailFlow({ authOrigin, add, onSuccess }) {
2073
2202
  const [stage, setStage] = React3.useState("enter");
2074
2203
  const [email, setEmail] = React3.useState("");
2075
2204
  const [emailError, setEmailError] = React3.useState(null);
@@ -2120,7 +2249,13 @@ function EmailFlow({ authOrigin, onSuccess }) {
2120
2249
  method: "POST",
2121
2250
  credentials: "include",
2122
2251
  headers: { "Content-Type": "application/json" },
2123
- body: JSON.stringify({ email, code, token })
2252
+ body: JSON.stringify({
2253
+ email,
2254
+ code,
2255
+ token,
2256
+ // Multi-account add-mode · see WalletFlow.
2257
+ ...add ? { add: true } : {}
2258
+ })
2124
2259
  });
2125
2260
  const json = await res.json();
2126
2261
  if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
@@ -2612,6 +2747,6 @@ function handleSudoRequired(body, args = {}) {
2612
2747
  return false;
2613
2748
  }
2614
2749
 
2615
- export { DEFAULT_CONFIG, DISPLAY_IDENTITY_KINDS, OcAccountChip, OcAccountPill, OcAddressInput, OcLinkedIdentities, OcSessionProvider, OcSignIn, OcSignInButton, buildSignInUrl, fetchOcLinkedIdentities, handleSudoRequired, redirectToSudo, useOcAddressSuggestion, useOcSession, useOptionalOcSession, useStepUpAuth, useWebAuthnList, useWebAuthnRegister };
2750
+ export { DEFAULT_CONFIG, DISPLAY_IDENTITY_KINDS, OcAccountChip, OcAccountPill, OcAddressInput, OcLinkedIdentities, OcSessionProvider, OcSignIn, OcSignInButton, buildAddAccountUrl, buildSignInUrl, fetchOcLinkedIdentities, handleSudoRequired, redirectToSudo, useOcAddressSuggestion, useOcSession, useOptionalOcSession, useStepUpAuth, useWebAuthnList, useWebAuthnRegister };
2616
2751
  //# sourceMappingURL=index.mjs.map
2617
2752
  //# sourceMappingURL=index.mjs.map