@orangecheck/auth-client 2.17.1 → 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;
@@ -48,6 +133,20 @@ function normalizeRosterEntry(raw) {
48
133
  lastSeenAt: raw.last_seen_at ?? raw.lastSeenAt ?? null
49
134
  };
50
135
  }
136
+ async function fetchHostRoster(cfg) {
137
+ try {
138
+ const res = await fetch(`${cfg.authOrigin}${cfg.mePath}`, {
139
+ method: "GET",
140
+ credentials: "include",
141
+ headers: { Accept: "application/json", ...tabSessionHeader() }
142
+ });
143
+ if (!res.ok) return [];
144
+ const body = await res.json();
145
+ return Array.isArray(body.roster) ? body.roster.map(normalizeRosterEntry).filter((r) => r !== null) : [];
146
+ } catch {
147
+ return [];
148
+ }
149
+ }
51
150
  function normalizeAccount(raw) {
52
151
  if (!raw) return null;
53
152
  const didOc = raw.did_oc ?? raw.didOc;
@@ -76,14 +175,50 @@ function OcSessionProvider({
76
175
  const [roster, setRoster] = React3.useState([]);
77
176
  const [status, setStatus] = React3.useState("loading");
78
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
+ );
79
203
  const refresh = React3.useCallback(async () => {
80
204
  if (typeof window === "undefined") return;
81
205
  try {
82
- const res = await fetch(cfg.mePath, {
206
+ let pin = readTabSession();
207
+ let res = await fetch(cfg.mePath, {
83
208
  method: "GET",
84
209
  credentials: "include",
85
- headers: { Accept: "application/json" }
210
+ headers: { Accept: "application/json", ...tabSessionHeader() }
86
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
+ }
87
222
  if (res.status === 401) {
88
223
  setAccount(null);
89
224
  setRoster([]);
@@ -98,19 +233,32 @@ function OcSessionProvider({
98
233
  }
99
234
  const body = await res.json();
100
235
  const acct = normalizeAccount(body.account);
236
+ if (acct && pin && acct.didOc !== pin.didOc) {
237
+ clearTabSession();
238
+ pin = null;
239
+ }
240
+ setTabPinned(pin !== null);
101
241
  const rosterEntries = Array.isArray(body.roster) ? body.roster.map(normalizeRosterEntry).filter((r) => r !== null) : [];
102
242
  setAccount(acct);
103
243
  setRoster(rosterEntries);
104
244
  setStatus(acct ? "authenticated" : "anonymous");
105
245
  setError(null);
246
+ if (acct && !pin) void pinThisTab(acct.didOc);
247
+ const currentOrigin = typeof window !== "undefined" ? window.location.origin : null;
248
+ if (acct && rosterEntries.length === 0 && currentOrigin !== null && !cfg.authOrigin.startsWith(currentOrigin)) {
249
+ const peers = await fetchHostRoster(cfg);
250
+ if (peers.length > 0) setRoster(peers);
251
+ }
106
252
  } catch (err) {
107
253
  setStatus("error");
108
254
  setError(err instanceof Error ? err : new Error(String(err)));
109
255
  }
110
- }, [cfg.mePath]);
256
+ }, [cfg, pinThisTab]);
111
257
  React3.useEffect(() => {
258
+ consumeTabAdoptMarker();
112
259
  void refresh();
113
260
  }, [refresh]);
261
+ React3.useEffect(() => installTabFetchInterceptor(cfg.authOrigin), [cfg.authOrigin]);
114
262
  const signOut = React3.useCallback(
115
263
  async (opts) => {
116
264
  const scope = opts?.scope ?? "all";
@@ -120,6 +268,10 @@ function OcSessionProvider({
120
268
  const res = await fetch(url.toString(), {
121
269
  method: "POST",
122
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() },
123
275
  // `keepalive` lets the logout round-trip complete even if
124
276
  // the caller hard-navigates away in the same tick (e.g.
125
277
  // `<OcAccountMenu>` redirects home immediately on sign-out).
@@ -127,11 +279,15 @@ function OcSessionProvider({
127
279
  // and the `.ochk.io` cookie may never get cleared.
128
280
  keepalive: true
129
281
  });
282
+ clearTabSession();
283
+ setTabPinned(false);
130
284
  if (scope === "current" && res.ok) {
131
285
  await refresh();
132
286
  return;
133
287
  }
134
288
  } catch {
289
+ clearTabSession();
290
+ setTabPinned(false);
135
291
  }
136
292
  setAccount(null);
137
293
  setRoster([]);
@@ -145,7 +301,7 @@ function OcSessionProvider({
145
301
  const res = await fetch(`${cfg.authOrigin}/api/auth/switch`, {
146
302
  method: "POST",
147
303
  credentials: "include",
148
- headers: { "Content-Type": "application/json" },
304
+ headers: { "Content-Type": "application/json", ...tabSessionHeader() },
149
305
  body: JSON.stringify({ did_oc: didOc })
150
306
  });
151
307
  if (!res.ok) {
@@ -157,6 +313,14 @@ function OcSessionProvider({
157
313
  }
158
314
  throw new Error(`[@orangecheck/auth-client] switchAccount failed: ${reason}`);
159
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
+ }
160
324
  await refresh();
161
325
  },
162
326
  [cfg.authOrigin, refresh]
@@ -174,7 +338,7 @@ function OcSessionProvider({
174
338
  const res = await fetch(`${cfg.authOrigin}/api/auth/account`, {
175
339
  method: "PATCH",
176
340
  credentials: "include",
177
- headers: { "Content-Type": "application/json" },
341
+ headers: { "Content-Type": "application/json", ...tabSessionHeader() },
178
342
  body: JSON.stringify({ display_identity: kind })
179
343
  });
180
344
  if (!res.ok) {
@@ -186,6 +350,14 @@ function OcSessionProvider({
186
350
  }
187
351
  throw new Error(`[@orangecheck/auth-client] setDisplayIdentity failed: ${reason}`);
188
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
+ }
189
361
  await refresh();
190
362
  },
191
363
  [cfg.authOrigin, refresh]
@@ -196,6 +368,7 @@ function OcSessionProvider({
196
368
  status,
197
369
  account,
198
370
  roster,
371
+ tabPinned,
199
372
  error,
200
373
  refresh,
201
374
  signOut,
@@ -208,6 +381,7 @@ function OcSessionProvider({
208
381
  status,
209
382
  account,
210
383
  roster,
384
+ tabPinned,
211
385
  error,
212
386
  refresh,
213
387
  signOut,
@@ -1733,6 +1907,18 @@ function safeReturnTo(input) {
1733
1907
  if (!candidate.startsWith("/") || candidate.startsWith("//")) return "/";
1734
1908
  return candidate;
1735
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
+ }
1736
1922
  function hardNavigate(target) {
1737
1923
  if (typeof window === "undefined") return;
1738
1924
  window.location.assign(target);
@@ -1751,7 +1937,20 @@ function OcSignIn({
1751
1937
  }) {
1752
1938
  const walletEnabled = paths?.wallet ?? true;
1753
1939
  const emailEnabled = paths?.email ?? true;
1754
- 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]);
1755
1954
  const [addMode, setAddMode] = React3.useState(Boolean(addProp));
1756
1955
  React3.useEffect(() => {
1757
1956
  if (typeof window === "undefined") return;
@@ -1775,18 +1974,20 @@ function OcSignIn({
1775
1974
  async (account) => {
1776
1975
  if (resolveReturnTo) {
1777
1976
  try {
1778
- hardNavigate(safeReturnTo(await resolveReturnTo(account)));
1977
+ const resolved = familyReturnTarget(await resolveReturnTo(account));
1978
+ hardNavigate(resolved ?? resolvedReturn);
1779
1979
  return;
1780
1980
  } catch {
1781
1981
  }
1782
1982
  }
1783
- hardNavigate(safeReturn);
1983
+ hardNavigate(resolvedReturn);
1784
1984
  },
1785
- [resolveReturnTo, safeReturn]
1985
+ [resolveReturnTo, resolvedReturn]
1786
1986
  );
1787
1987
  const handleSuccess = React3.useCallback(
1788
1988
  async (account, token, via) => {
1789
1989
  const proceed = () => {
1990
+ clearTabSession();
1790
1991
  if (onSuccess) onSuccess(account, token);
1791
1992
  else void navigate(account);
1792
1993
  };
@@ -1951,7 +2152,7 @@ function OcSignIn({
1951
2152
  }
1952
2153
  )
1953
2154
  ] }),
1954
- /* @__PURE__ */ jsx(ProviderSignIn, { authOrigin, returnTo: safeReturn }),
2155
+ /* @__PURE__ */ jsx(ProviderSignIn, { authOrigin, returnTo: resolvedReturn, add: addMode }),
1955
2156
  linkPrompt && /* @__PURE__ */ jsxs("label", { "data-oc-signin-linkalso": "", style: linkAlsoStyle, children: [
1956
2157
  /* @__PURE__ */ jsx(
1957
2158
  "input",
@@ -2021,7 +2222,8 @@ function ProviderIcon({ id }) {
2021
2222
  }
2022
2223
  function ProviderSignIn({
2023
2224
  authOrigin,
2024
- returnTo
2225
+ returnTo,
2226
+ add
2025
2227
  }) {
2026
2228
  const [providers, setProviders] = React3.useState([]);
2027
2229
  const [origin, setOrigin] = React3.useState("");
@@ -2037,7 +2239,7 @@ function ProviderSignIn({
2037
2239
  };
2038
2240
  }, [authOrigin]);
2039
2241
  if (providers.length === 0) return null;
2040
- const providerReturnTo = origin ? `${origin}${returnTo}` : returnTo;
2242
+ const providerReturnTo = returnTo.startsWith("/") ? origin ? `${origin}${returnTo}` : returnTo : returnTo;
2041
2243
  const line = { flex: 1, height: 1, background: "var(--border, #27272a)" };
2042
2244
  return /* @__PURE__ */ jsxs("div", { "data-oc-signin-providers": "", style: { marginTop: 20 }, children: [
2043
2245
  /* @__PURE__ */ jsxs(
@@ -2066,7 +2268,7 @@ function ProviderSignIn({
2066
2268
  {
2067
2269
  href: `${authOrigin}/api/auth/${p.id}/start?return_to=${encodeURIComponent(
2068
2270
  providerReturnTo
2069
- )}`,
2271
+ )}${add ? "&add=1" : ""}`,
2070
2272
  "data-oc-signin-provider": p.id,
2071
2273
  style: {
2072
2274
  display: "flex",
@@ -2747,6 +2949,6 @@ function handleSudoRequired(body, args = {}) {
2747
2949
  return false;
2748
2950
  }
2749
2951
 
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 };
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 };
2751
2953
  //# sourceMappingURL=index.mjs.map
2752
2954
  //# sourceMappingURL=index.mjs.map