@orangecheck/auth-client 2.17.2 → 2.18.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
@@ -27,6 +27,91 @@ function buildAddAccountUrl(cfg, returnTo) {
27
27
  u.searchParams.set("add", "1");
28
28
  return u.toString();
29
29
  }
30
+
31
+ // src/tab-session.ts
32
+ var TAB_SESSION_HEADER = "x-oc-tab-session";
33
+ var TAB_SESSION_STORAGE_KEY = "oc_tab_session";
34
+ var TAB_ADOPT_HASH = "#oc-adopt";
35
+ function readTabSession() {
36
+ if (typeof window === "undefined") return null;
37
+ try {
38
+ const raw = window.sessionStorage.getItem(TAB_SESSION_STORAGE_KEY);
39
+ if (!raw) return null;
40
+ const parsed = JSON.parse(raw);
41
+ if (typeof parsed.token !== "string" || parsed.token.length === 0) return null;
42
+ if (typeof parsed.didOc !== "string" || parsed.didOc.length === 0) return null;
43
+ return { token: parsed.token, didOc: parsed.didOc };
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ function writeTabSession(session) {
49
+ if (typeof window === "undefined") return;
50
+ try {
51
+ window.sessionStorage.setItem(TAB_SESSION_STORAGE_KEY, JSON.stringify(session));
52
+ } catch {
53
+ }
54
+ }
55
+ function clearTabSession() {
56
+ if (typeof window === "undefined") return;
57
+ try {
58
+ window.sessionStorage.removeItem(TAB_SESSION_STORAGE_KEY);
59
+ } catch {
60
+ }
61
+ }
62
+ function tabSessionHeader() {
63
+ const pin = readTabSession();
64
+ return pin ? { [TAB_SESSION_HEADER]: pin.token } : {};
65
+ }
66
+ function isPinnableUrl(url, authOrigin) {
67
+ if (typeof window === "undefined") return false;
68
+ try {
69
+ const u = new URL(url, window.location.href);
70
+ return u.origin === window.location.origin || u.origin === authOrigin;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ function installTabFetchInterceptor(authOrigin) {
76
+ if (typeof window === "undefined") return () => {
77
+ };
78
+ const original = window.fetch;
79
+ const wrapped = (input, init) => {
80
+ try {
81
+ const pin = readTabSession();
82
+ if (pin) {
83
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
84
+ if (isPinnableUrl(url, authOrigin)) {
85
+ const headers = new Headers(
86
+ init?.headers ?? (input instanceof Request ? input.headers : void 0)
87
+ );
88
+ if (!headers.has("authorization") && !headers.has(TAB_SESSION_HEADER)) {
89
+ headers.set(TAB_SESSION_HEADER, pin.token);
90
+ init = { ...init, headers };
91
+ }
92
+ }
93
+ }
94
+ } catch {
95
+ }
96
+ return original.call(window, input, init);
97
+ };
98
+ window.fetch = wrapped;
99
+ return () => {
100
+ if (window.fetch === wrapped) window.fetch = original;
101
+ };
102
+ }
103
+ function consumeTabAdoptMarker() {
104
+ if (typeof window === "undefined") return false;
105
+ if (!window.location.hash.includes(TAB_ADOPT_HASH.slice(1))) return false;
106
+ clearTabSession();
107
+ try {
108
+ const url = new URL(window.location.href);
109
+ url.hash = "";
110
+ window.history.replaceState(window.history.state, "", url.toString());
111
+ } catch {
112
+ }
113
+ return true;
114
+ }
30
115
  var SessionContext = React3.createContext(null);
31
116
  function normalizeDisplayIdentity(raw, didOc) {
32
117
  const di = raw.display_identity ?? raw.displayIdentity;
@@ -53,7 +138,7 @@ async function fetchHostRoster(cfg) {
53
138
  const res = await fetch(`${cfg.authOrigin}${cfg.mePath}`, {
54
139
  method: "GET",
55
140
  credentials: "include",
56
- headers: { Accept: "application/json" }
141
+ headers: { Accept: "application/json", ...tabSessionHeader() }
57
142
  });
58
143
  if (!res.ok) return [];
59
144
  const body = await res.json();
@@ -90,14 +175,50 @@ function OcSessionProvider({
90
175
  const [roster, setRoster] = React3.useState([]);
91
176
  const [status, setStatus] = React3.useState("loading");
92
177
  const [error, setError] = React3.useState(null);
178
+ const [tabPinned, setTabPinned] = React3.useState(false);
179
+ const pinInFlightRef = React3.useRef(false);
180
+ const pinThisTab = React3.useCallback(
181
+ async (didOc) => {
182
+ if (pinInFlightRef.current || readTabSession()) return;
183
+ pinInFlightRef.current = true;
184
+ try {
185
+ const res = await fetch(`${cfg.authOrigin}/api/auth/tab`, {
186
+ method: "POST",
187
+ credentials: "include",
188
+ headers: { Accept: "application/json" }
189
+ });
190
+ if (!res.ok) return;
191
+ const body = await res.json();
192
+ if (body.ok && typeof body.token === "string" && body.account?.did_oc === didOc) {
193
+ writeTabSession({ token: body.token, didOc });
194
+ setTabPinned(true);
195
+ }
196
+ } catch {
197
+ } finally {
198
+ pinInFlightRef.current = false;
199
+ }
200
+ },
201
+ [cfg.authOrigin]
202
+ );
93
203
  const refresh = React3.useCallback(async () => {
94
204
  if (typeof window === "undefined") return;
95
205
  try {
96
- const res = await fetch(cfg.mePath, {
206
+ let pin = readTabSession();
207
+ let res = await fetch(cfg.mePath, {
97
208
  method: "GET",
98
209
  credentials: "include",
99
- headers: { Accept: "application/json" }
210
+ headers: { Accept: "application/json", ...tabSessionHeader() }
100
211
  });
212
+ if (res.status === 401 && pin) {
213
+ clearTabSession();
214
+ pin = null;
215
+ setTabPinned(false);
216
+ res = await fetch(cfg.mePath, {
217
+ method: "GET",
218
+ credentials: "include",
219
+ headers: { Accept: "application/json" }
220
+ });
221
+ }
101
222
  if (res.status === 401) {
102
223
  setAccount(null);
103
224
  setRoster([]);
@@ -112,11 +233,17 @@ function OcSessionProvider({
112
233
  }
113
234
  const body = await res.json();
114
235
  const acct = normalizeAccount(body.account);
236
+ if (acct && pin && acct.didOc !== pin.didOc) {
237
+ clearTabSession();
238
+ pin = null;
239
+ }
240
+ setTabPinned(pin !== null);
115
241
  const rosterEntries = Array.isArray(body.roster) ? body.roster.map(normalizeRosterEntry).filter((r) => r !== null) : [];
116
242
  setAccount(acct);
117
243
  setRoster(rosterEntries);
118
244
  setStatus(acct ? "authenticated" : "anonymous");
119
245
  setError(null);
246
+ if (acct && !pin) void pinThisTab(acct.didOc);
120
247
  const currentOrigin = typeof window !== "undefined" ? window.location.origin : null;
121
248
  if (acct && rosterEntries.length === 0 && currentOrigin !== null && !cfg.authOrigin.startsWith(currentOrigin)) {
122
249
  const peers = await fetchHostRoster(cfg);
@@ -126,10 +253,12 @@ function OcSessionProvider({
126
253
  setStatus("error");
127
254
  setError(err instanceof Error ? err : new Error(String(err)));
128
255
  }
129
- }, [cfg]);
256
+ }, [cfg, pinThisTab]);
130
257
  React3.useEffect(() => {
258
+ consumeTabAdoptMarker();
131
259
  void refresh();
132
260
  }, [refresh]);
261
+ React3.useEffect(() => installTabFetchInterceptor(cfg.authOrigin), [cfg.authOrigin]);
133
262
  const signOut = React3.useCallback(
134
263
  async (opts) => {
135
264
  const scope = opts?.scope ?? "all";
@@ -139,6 +268,10 @@ function OcSessionProvider({
139
268
  const res = await fetch(url.toString(), {
140
269
  method: "POST",
141
270
  credentials: "include",
271
+ // Per-tab · carry the pin so scope='current' signs out
272
+ // the account THIS tab is operating as, not whichever
273
+ // account the shared cookie happens to point at.
274
+ headers: { ...tabSessionHeader() },
142
275
  // `keepalive` lets the logout round-trip complete even if
143
276
  // the caller hard-navigates away in the same tick (e.g.
144
277
  // `<OcAccountMenu>` redirects home immediately on sign-out).
@@ -146,11 +279,15 @@ function OcSessionProvider({
146
279
  // and the `.ochk.io` cookie may never get cleared.
147
280
  keepalive: true
148
281
  });
282
+ clearTabSession();
283
+ setTabPinned(false);
149
284
  if (scope === "current" && res.ok) {
150
285
  await refresh();
151
286
  return;
152
287
  }
153
288
  } catch {
289
+ clearTabSession();
290
+ setTabPinned(false);
154
291
  }
155
292
  setAccount(null);
156
293
  setRoster([]);
@@ -164,7 +301,7 @@ function OcSessionProvider({
164
301
  const res = await fetch(`${cfg.authOrigin}/api/auth/switch`, {
165
302
  method: "POST",
166
303
  credentials: "include",
167
- headers: { "Content-Type": "application/json" },
304
+ headers: { "Content-Type": "application/json", ...tabSessionHeader() },
168
305
  body: JSON.stringify({ did_oc: didOc })
169
306
  });
170
307
  if (!res.ok) {
@@ -176,6 +313,14 @@ function OcSessionProvider({
176
313
  }
177
314
  throw new Error(`[@orangecheck/auth-client] switchAccount failed: ${reason}`);
178
315
  }
316
+ try {
317
+ const body = await res.json();
318
+ if (typeof body.token === "string" && body.account?.did_oc) {
319
+ writeTabSession({ token: body.token, didOc: body.account.did_oc });
320
+ setTabPinned(true);
321
+ }
322
+ } catch {
323
+ }
179
324
  await refresh();
180
325
  },
181
326
  [cfg.authOrigin, refresh]
@@ -193,7 +338,7 @@ function OcSessionProvider({
193
338
  const res = await fetch(`${cfg.authOrigin}/api/auth/account`, {
194
339
  method: "PATCH",
195
340
  credentials: "include",
196
- headers: { "Content-Type": "application/json" },
341
+ headers: { "Content-Type": "application/json", ...tabSessionHeader() },
197
342
  body: JSON.stringify({ display_identity: kind })
198
343
  });
199
344
  if (!res.ok) {
@@ -205,6 +350,14 @@ function OcSessionProvider({
205
350
  }
206
351
  throw new Error(`[@orangecheck/auth-client] setDisplayIdentity failed: ${reason}`);
207
352
  }
353
+ try {
354
+ const body = await res.json();
355
+ const pin = readTabSession();
356
+ if (pin && typeof body.token === "string" && body.account?.did_oc === pin.didOc) {
357
+ writeTabSession({ token: body.token, didOc: pin.didOc });
358
+ }
359
+ } catch {
360
+ }
208
361
  await refresh();
209
362
  },
210
363
  [cfg.authOrigin, refresh]
@@ -215,6 +368,7 @@ function OcSessionProvider({
215
368
  status,
216
369
  account,
217
370
  roster,
371
+ tabPinned,
218
372
  error,
219
373
  refresh,
220
374
  signOut,
@@ -227,6 +381,7 @@ function OcSessionProvider({
227
381
  status,
228
382
  account,
229
383
  roster,
384
+ tabPinned,
230
385
  error,
231
386
  refresh,
232
387
  signOut,
@@ -1752,6 +1907,18 @@ function safeReturnTo(input) {
1752
1907
  if (!candidate.startsWith("/") || candidate.startsWith("//")) return "/";
1753
1908
  return candidate;
1754
1909
  }
1910
+ function familyReturnTarget(input) {
1911
+ if (typeof input !== "string" || input.length === 0) return void 0;
1912
+ if (input.startsWith("/") && !input.startsWith("//")) return input;
1913
+ try {
1914
+ const u = new URL(input);
1915
+ if (u.protocol !== "https:") return void 0;
1916
+ const host = u.hostname.toLowerCase();
1917
+ if (host === "ochk.io" || host.endsWith(".ochk.io")) return u.toString();
1918
+ } catch {
1919
+ }
1920
+ return void 0;
1921
+ }
1755
1922
  function hardNavigate(target) {
1756
1923
  if (typeof window === "undefined") return;
1757
1924
  window.location.assign(target);
@@ -1770,7 +1937,20 @@ function OcSignIn({
1770
1937
  }) {
1771
1938
  const walletEnabled = paths?.wallet ?? true;
1772
1939
  const emailEnabled = paths?.email ?? true;
1773
- const safeReturn = safeReturnTo(returnTo);
1940
+ const [resolvedReturn, setResolvedReturn] = React3.useState(
1941
+ () => familyReturnTarget(returnTo) ?? safeReturnTo(returnTo)
1942
+ );
1943
+ React3.useEffect(() => {
1944
+ const fromProp = familyReturnTarget(returnTo);
1945
+ if (fromProp) {
1946
+ setResolvedReturn(fromProp);
1947
+ return;
1948
+ }
1949
+ if (typeof window === "undefined") return;
1950
+ const q = new URLSearchParams(window.location.search);
1951
+ const fromQuery = familyReturnTarget(q.get("return_to")) ?? familyReturnTarget(q.get("next"));
1952
+ setResolvedReturn(fromQuery ?? "/");
1953
+ }, [returnTo]);
1774
1954
  const [addMode, setAddMode] = React3.useState(Boolean(addProp));
1775
1955
  React3.useEffect(() => {
1776
1956
  if (typeof window === "undefined") return;
@@ -1794,18 +1974,20 @@ function OcSignIn({
1794
1974
  async (account) => {
1795
1975
  if (resolveReturnTo) {
1796
1976
  try {
1797
- hardNavigate(safeReturnTo(await resolveReturnTo(account)));
1977
+ const resolved = familyReturnTarget(await resolveReturnTo(account));
1978
+ hardNavigate(resolved ?? resolvedReturn);
1798
1979
  return;
1799
1980
  } catch {
1800
1981
  }
1801
1982
  }
1802
- hardNavigate(safeReturn);
1983
+ hardNavigate(resolvedReturn);
1803
1984
  },
1804
- [resolveReturnTo, safeReturn]
1985
+ [resolveReturnTo, resolvedReturn]
1805
1986
  );
1806
1987
  const handleSuccess = React3.useCallback(
1807
1988
  async (account, token, via) => {
1808
1989
  const proceed = () => {
1990
+ clearTabSession();
1809
1991
  if (onSuccess) onSuccess(account, token);
1810
1992
  else void navigate(account);
1811
1993
  };
@@ -1970,7 +2152,7 @@ function OcSignIn({
1970
2152
  }
1971
2153
  )
1972
2154
  ] }),
1973
- /* @__PURE__ */ jsx(ProviderSignIn, { authOrigin, returnTo: safeReturn }),
2155
+ /* @__PURE__ */ jsx(ProviderSignIn, { authOrigin, returnTo: resolvedReturn, add: addMode }),
1974
2156
  linkPrompt && /* @__PURE__ */ jsxs("label", { "data-oc-signin-linkalso": "", style: linkAlsoStyle, children: [
1975
2157
  /* @__PURE__ */ jsx(
1976
2158
  "input",
@@ -2040,7 +2222,8 @@ function ProviderIcon({ id }) {
2040
2222
  }
2041
2223
  function ProviderSignIn({
2042
2224
  authOrigin,
2043
- returnTo
2225
+ returnTo,
2226
+ add
2044
2227
  }) {
2045
2228
  const [providers, setProviders] = React3.useState([]);
2046
2229
  const [origin, setOrigin] = React3.useState("");
@@ -2056,7 +2239,7 @@ function ProviderSignIn({
2056
2239
  };
2057
2240
  }, [authOrigin]);
2058
2241
  if (providers.length === 0) return null;
2059
- const providerReturnTo = origin ? `${origin}${returnTo}` : returnTo;
2242
+ const providerReturnTo = returnTo.startsWith("/") ? origin ? `${origin}${returnTo}` : returnTo : returnTo;
2060
2243
  const line = { flex: 1, height: 1, background: "var(--border, #27272a)" };
2061
2244
  return /* @__PURE__ */ jsxs("div", { "data-oc-signin-providers": "", style: { marginTop: 20 }, children: [
2062
2245
  /* @__PURE__ */ jsxs(
@@ -2085,7 +2268,7 @@ function ProviderSignIn({
2085
2268
  {
2086
2269
  href: `${authOrigin}/api/auth/${p.id}/start?return_to=${encodeURIComponent(
2087
2270
  providerReturnTo
2088
- )}`,
2271
+ )}${add ? "&add=1" : ""}`,
2089
2272
  "data-oc-signin-provider": p.id,
2090
2273
  style: {
2091
2274
  display: "flex",
@@ -2766,6 +2949,6 @@ function handleSudoRequired(body, args = {}) {
2766
2949
  return false;
2767
2950
  }
2768
2951
 
2769
- export { DEFAULT_CONFIG, DISPLAY_IDENTITY_KINDS, OcAccountChip, OcAccountPill, OcAddressInput, OcLinkedIdentities, OcSessionProvider, OcSignIn, OcSignInButton, buildAddAccountUrl, buildSignInUrl, fetchOcLinkedIdentities, handleSudoRequired, redirectToSudo, useOcAddressSuggestion, useOcSession, useOptionalOcSession, useStepUpAuth, useWebAuthnList, useWebAuthnRegister };
2952
+ export { DEFAULT_CONFIG, DISPLAY_IDENTITY_KINDS, OcAccountChip, OcAccountPill, OcAddressInput, OcLinkedIdentities, OcSessionProvider, OcSignIn, OcSignInButton, TAB_ADOPT_HASH, TAB_SESSION_HEADER, TAB_SESSION_STORAGE_KEY, buildAddAccountUrl, buildSignInUrl, clearTabSession, consumeTabAdoptMarker, fetchOcLinkedIdentities, handleSudoRequired, installTabFetchInterceptor, readTabSession, redirectToSudo, tabSessionHeader, useOcAddressSuggestion, useOcSession, useOptionalOcSession, useStepUpAuth, useWebAuthnList, useWebAuthnRegister, writeTabSession };
2770
2953
  //# sourceMappingURL=index.mjs.map
2771
2954
  //# sourceMappingURL=index.mjs.map