@oxyhq/services 11.1.1 → 12.0.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.
Files changed (55) hide show
  1. package/lib/commonjs/index.js +2 -0
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/OxyProvider.js +0 -2
  4. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  5. package/lib/commonjs/ui/context/OxyContext.js +79 -10
  6. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  7. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +24 -3
  8. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  9. package/lib/commonjs/ui/index.js.map +1 -1
  10. package/lib/commonjs/ui/utils/storageHelpers.js +2 -1
  11. package/lib/commonjs/ui/utils/storageHelpers.js.map +1 -1
  12. package/lib/module/index.js +2 -0
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/module/ui/components/OxyProvider.js +0 -2
  15. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  16. package/lib/module/ui/context/OxyContext.js +80 -11
  17. package/lib/module/ui/context/OxyContext.js.map +1 -1
  18. package/lib/module/ui/context/hooks/useAuthOperations.js +24 -3
  19. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  20. package/lib/module/ui/index.js +2 -0
  21. package/lib/module/ui/index.js.map +1 -1
  22. package/lib/module/ui/utils/storageHelpers.js +2 -1
  23. package/lib/module/ui/utils/storageHelpers.js.map +1 -1
  24. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  25. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +0 -10
  27. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +9 -1
  29. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/ui/types/navigation.d.ts +0 -17
  32. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/ui/utils/storageHelpers.d.ts +11 -0
  34. package/lib/typescript/commonjs/ui/utils/storageHelpers.d.ts.map +1 -1
  35. package/lib/typescript/module/index.d.ts.map +1 -1
  36. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  37. package/lib/typescript/module/ui/context/OxyContext.d.ts +0 -10
  38. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  39. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +9 -1
  40. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  41. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  42. package/lib/typescript/module/ui/types/navigation.d.ts +0 -17
  43. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  44. package/lib/typescript/module/ui/utils/storageHelpers.d.ts +11 -0
  45. package/lib/typescript/module/ui/utils/storageHelpers.d.ts.map +1 -1
  46. package/lib/typescript/types/react-native-classname.d.ts +39 -0
  47. package/package.json +2 -2
  48. package/src/index.ts +2 -0
  49. package/src/types/react-native-classname.d.ts +39 -0
  50. package/src/ui/components/OxyProvider.tsx +0 -2
  51. package/src/ui/context/OxyContext.tsx +81 -19
  52. package/src/ui/context/hooks/useAuthOperations.ts +35 -2
  53. package/src/ui/index.ts +2 -0
  54. package/src/ui/types/navigation.ts +0 -17
  55. package/src/ui/utils/storageHelpers.ts +12 -0
@@ -0,0 +1,39 @@
1
+ // Robust NativeWind className typings for React Native's `ScrollView` & `FlatList`.
2
+ //
3
+ // NativeWind 5's engine (`react-native-css`) augments `ScrollViewProps` and
4
+ // `FlatListProps` with className props, but its declarations carry heritage
5
+ // clauses (`interface ScrollViewProps extends ViewProps, ScrollViewPropsIOS, …`)
6
+ // and route `FlatList`'s `contentContainerClassName` through a deep
7
+ // `@react-native/virtualized-lists` → `ScrollViewProps` augmentation. Under
8
+ // React Native 0.85 that heritage no longer matches, and the nested
9
+ // `@react-native/virtualized-lists` copy does not resolve from a CONSUMER's
10
+ // `node_modules` — so when this package's source is type-checked through the
11
+ // `react-native` export condition (which resolves `@oxyhq/services` to raw
12
+ // `src/`), those className members silently drop and consumer `tsc` fails with
13
+ // TS2769 on screens that use `contentContainerClassName` (e.g.
14
+ // ManageAccountScreen, ConnectedAppsScreen).
15
+ //
16
+ // These heritage-free augmentations add the className props directly to the two
17
+ // interfaces, so resolution never depends on the deep virtualized-lists copy or
18
+ // on react-native-css's heritage clauses. The plain `import 'react-native'`
19
+ // makes this a module so the `declare module` is treated as an augmentation
20
+ // (merge) rather than a redeclaration. Consumers load it via the
21
+ // `/// <reference path>` directives in `src/index.ts` and `src/ui/index.ts`.
22
+
23
+ import 'react-native';
24
+
25
+ declare module 'react-native' {
26
+ interface ScrollViewProps {
27
+ className?: string;
28
+ contentContainerClassName?: string;
29
+ indicatorClassName?: string;
30
+ }
31
+
32
+ interface FlatListProps<ItemT> {
33
+ className?: string;
34
+ contentContainerClassName?: string;
35
+ columnWrapperClassName?: string;
36
+ ListHeaderComponentClassName?: string;
37
+ ListFooterComponentClassName?: string;
38
+ }
39
+ }
@@ -113,7 +113,6 @@ const OxyProvider: FC<OxyProviderProps> = ({
113
113
  baseURL,
114
114
  authWebUrl,
115
115
  authRedirectUri,
116
- disableAutoSso,
117
116
  queryClient: providedQueryClient,
118
117
  }) => {
119
118
 
@@ -309,7 +308,6 @@ const OxyProvider: FC<OxyProviderProps> = ({
309
308
  authRedirectUri={authRedirectUri}
310
309
  storageKeyPrefix={storageKeyPrefix}
311
310
  clientId={clientId}
312
- disableAutoSso={disableAutoSso}
313
311
  onAuthStateChange={onAuthStateChange as OxyContextProviderProps['onAuthStateChange']}
314
312
  >
315
313
  {children}
@@ -28,6 +28,7 @@ import {
28
28
  ssoAttemptedKey,
29
29
  isCentralIdPOrigin,
30
30
  guardActive,
31
+ allowSsoBounce,
31
32
  ssoNavigate,
32
33
  buildSsoBounceUrl,
33
34
  consumeSsoReturn,
@@ -196,16 +197,6 @@ export interface OxyContextProviderProps {
196
197
  * for the cross-app device sign-in flow. See {@link OxyContextState.clientId}.
197
198
  */
198
199
  clientId?: string;
199
- /**
200
- * When `true`, skips ONLY the terminal `sso-bounce` cold-boot step — the
201
- * force-redirect to `auth.<apex>/sso?prompt=none` that fires for a visitor
202
- * with no recoverable local session. Every other cold-boot step still runs
203
- * (callback consume, FedCM silent, `/auth/silent` iframe, stored-session,
204
- * cookie-restore), so a returning signed-in user is still silently
205
- * restored; only the bounce for a truly anonymous visitor is suppressed.
206
- * Default `false`.
207
- */
208
- disableAutoSso?: boolean;
209
200
  onAuthStateChange?: (user: User | null) => void;
210
201
  onError?: (error: ApiError) => void;
211
202
  }
@@ -462,7 +453,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
462
453
  authRedirectUri,
463
454
  storageKeyPrefix = 'oxy_session',
464
455
  clientId: clientIdProp,
465
- disableAutoSso = false,
466
456
  onAuthStateChange,
467
457
  onError,
468
458
  }) => {
@@ -631,6 +621,25 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
631
621
  return storageReady.promise;
632
622
  }, [storageReady]);
633
623
 
624
+ // Clear the durable "had a signed-in Oxy session before" hint.
625
+ //
626
+ // The hint (`storageKeys.priorSession`) is WRITTEN whenever a session is
627
+ // established/restored — every commit funnels through `persistSessionDurably`,
628
+ // and the stored-session reload winner sets it directly to backfill
629
+ // pre-existing installs. It lives in the SAME `storageKeyPrefix`-scoped
630
+ // durable store as the session ids, so it SURVIVES a session expiring; it is
631
+ // cleared ONLY here, on EXPLICIT full sign-out (wired into `clearAllAccountData`
632
+ // and the `useAuthOperations` logout paths — never the passive token-expiry
633
+ // path). At cold boot the hint is read into `hadPriorSession` and feeds
634
+ // `allowSsoBounce`: a RETURNING visitor (hint present) whose local session has
635
+ // lapsed still gets ONE terminal `/sso` establish bounce so a central-only
636
+ // cross-domain session recovers, while a truly first-time anonymous visitor is
637
+ // never force-redirected.
638
+ const clearPriorSessionHint = useCallback(async (): Promise<void> => {
639
+ const readyStorage = await getReadyStorage();
640
+ await readyStorage.removeItem(storageKeys.priorSession);
641
+ }, [getReadyStorage, storageKeys.priorSession]);
642
+
634
643
  useEffect(() => {
635
644
  let mounted = true;
636
645
  createPlatformStorage()
@@ -717,6 +726,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
717
726
  updateSessions,
718
727
  saveActiveSessionId,
719
728
  clearSessionState,
729
+ clearPriorSessionHint,
720
730
  switchSession,
721
731
  applyLanguagePreference,
722
732
  onAuthStateChange,
@@ -745,12 +755,18 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
745
755
  // Clear session state (sessions, activeSessionId, storage)
746
756
  await clearSessionState();
747
757
 
758
+ // Explicit FULL sign-out: drop the durable returning-user hint so the next
759
+ // cold boot treats this device as a first-time anonymous visitor (no forced
760
+ // `/sso` bounce). NOT done on the passive token-expiry path, so an expired
761
+ // session still recovers via a returning-user bounce.
762
+ await clearPriorSessionHint();
763
+
748
764
  // Reset account store
749
765
  useAccountStore.getState().reset();
750
766
 
751
767
  // Clear HTTP service cache
752
768
  oxyServices.clearCache();
753
- }, [queryClient, storage, clearSessionState, logger, oxyServices]);
769
+ }, [queryClient, storage, clearSessionState, clearPriorSessionHint, logger, oxyServices]);
754
770
 
755
771
  const { getDeviceSessions, logoutAllDeviceSessions, updateDeviceName } = useDeviceManagement({
756
772
  oxyServices,
@@ -831,7 +847,13 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
831
847
  sessionIds.push(sessionId);
832
848
  await readyStorage.setItem(storageKeys.sessionIds, JSON.stringify(sessionIds));
833
849
  }
834
- }, [getReadyStorage, logger, storageKeys.activeSessionId, storageKeys.sessionIds]);
850
+ // A session is now durably committed — set the returning-user hint so a
851
+ // future cold boot whose local session has lapsed still gets ONE `/sso`
852
+ // establish bounce (see `markPriorSessionHint`). Every web commit path
853
+ // (FedCM / silent iframe / SSO return / password / cookie restore) funnels
854
+ // through here, so this is the single chokepoint for the hint.
855
+ await readyStorage.setItem(storageKeys.priorSession, '1');
856
+ }, [getReadyStorage, logger, storageKeys.activeSessionId, storageKeys.sessionIds, storageKeys.priorSession]);
835
857
 
836
858
  // Refs so the cold-boot restore can plant session state without widening its
837
859
  // dependency array (mirrors the existing ref pattern above).
@@ -1073,6 +1095,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1073
1095
  // immediately so the loading screen clears without waiting for the
1074
1096
  // remaining cold-boot steps to be evaluated/short-circuited (idempotent).
1075
1097
  markAuthResolvedRef.current();
1098
+ // Backfill the returning-user hint. A reload winner already had the hint
1099
+ // set at original sign-in, but pre-existing installs (signed in before
1100
+ // this hint shipped) get it set here so their NEXT lapse-and-return still
1101
+ // earns one `/sso` establish bounce. `storage` is non-null (guarded at
1102
+ // the top of this callback); best-effort, never blocks restore.
1103
+ await storage.setItem(storageKeys.priorSession, '1');
1076
1104
  return true;
1077
1105
  } catch (switchError) {
1078
1106
  // Silently handle expected errors (invalid sessions, timeouts, network issues)
@@ -1102,6 +1130,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1102
1130
  storage,
1103
1131
  storageKeys.activeSessionId,
1104
1132
  storageKeys.sessionIds,
1133
+ storageKeys.priorSession,
1105
1134
  ]);
1106
1135
 
1107
1136
  // Shared in-flight `runSsoReturn` promise — see the CONCURRENCY note on
@@ -1213,6 +1242,27 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1213
1242
  // `stored-session` skips, this stays false, and the probes run as before.
1214
1243
  let storedSessionRestored = false;
1215
1244
 
1245
+ // FIX-B smart-gate input: has this device/app EVER had a signed-in Oxy
1246
+ // session (the durable `priorSession` hint, set on every commit, cleared
1247
+ // only on explicit full sign-out)? Read ONCE here — synchronously usable by
1248
+ // the terminal `sso-bounce` `enabled` gate below — so a RETURNING visitor
1249
+ // whose local session has lapsed still earns one `/sso` establish bounce,
1250
+ // while a truly first-time anonymous visitor is never force-redirected.
1251
+ // `storage` is non-null (guarded above); a read failure is treated as "no
1252
+ // prior session" (fail safe toward anonymous-browse).
1253
+ let hadPriorSession = false;
1254
+ try {
1255
+ hadPriorSession = (await storage.getItem(storageKeys.priorSession)) === '1';
1256
+ } catch (priorSessionReadError) {
1257
+ if (__DEV__) {
1258
+ loggerUtil.debug(
1259
+ 'Failed to read prior-session hint (treating as first-time visitor)',
1260
+ { component: 'OxyContext', method: 'restoreSessionsFromStorage' },
1261
+ priorSessionReadError as unknown,
1262
+ );
1263
+ }
1264
+ }
1265
+
1216
1266
  try {
1217
1267
  const outcome = await runColdBoot<true>({
1218
1268
  steps: [
@@ -1400,11 +1450,23 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1400
1450
  // still active (loop + self-heal protection).
1401
1451
  id: 'sso-bounce',
1402
1452
  enabled: () => {
1403
- // Opt-out: when the consumer disabled auto-SSO, never bounce a
1404
- // truly-anonymous visitor to the central IdP. All other restore
1405
- // steps already ran above, so a signed-in user is still
1406
- // recovered; only this terminal force-bounce is suppressed.
1407
- if (disableAutoSso) {
1453
+ // Smart gate (SDK-owned, shared with `WebOxyProvider` via core's
1454
+ // `allowSsoBounce`). The terminal `/sso` establish-bounce is the
1455
+ // ONLY cold-boot step that can recover a session living SOLELY at
1456
+ // the central IdP (a cross-apex RP whose local session expired),
1457
+ // and it is what plants the per-apex `fedcm_session` cookie the
1458
+ // earlier silent-iframe step relies on. So it is allowed iff a
1459
+ // prior-signed-in hint exists OR a local session was recovered this
1460
+ // boot (`storedSessionRestored`, always false here — an earlier step
1461
+ // would have won — but passed for spec fidelity): a RETURNING user
1462
+ // still gets ONE bounce, while a truly first-time anonymous visitor
1463
+ // does NOT (anonymous browse). The per-tab loop guards below
1464
+ // (`ssoNoSessionKey`, `ssoAttemptedKey`, `guardActive`) still cap an
1465
+ // allowed bounce at one per cold boot.
1466
+ if (!allowSsoBounce({
1467
+ hasPriorSession: hadPriorSession,
1468
+ hasLocalSession: storedSessionRestored,
1469
+ })) {
1408
1470
  return false;
1409
1471
  }
1410
1472
  if (!isWebBrowser() || window.top !== window.self) {
@@ -1497,11 +1559,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1497
1559
  }, [
1498
1560
  oxyServices,
1499
1561
  storage,
1562
+ storageKeys.priorSession,
1500
1563
  restoreViaRefreshCookie,
1501
1564
  restoreStoredSession,
1502
1565
  runSsoReturn,
1503
1566
  markAuthResolved,
1504
- disableAutoSso,
1505
1567
  ]);
1506
1568
 
1507
1569
  useEffect(() => {
@@ -20,6 +20,14 @@ export interface UseAuthOperationsOptions {
20
20
  updateSessions: (sessions: ClientSession[], options?: { merge?: boolean }) => void;
21
21
  saveActiveSessionId: (sessionId: string) => Promise<void>;
22
22
  clearSessionState: () => Promise<void>;
23
+ /**
24
+ * Clear the durable returning-user hint (`storageKeys.priorSession`). Called
25
+ * ONLY on EXPLICIT full sign-out — alongside `clearSsoBounceState()` — so the
26
+ * next cold boot treats this device as a first-time anonymous visitor (no
27
+ * forced `/sso` bounce). NEVER called on the passive token-expiry path, so an
28
+ * expired session still recovers via a returning-user bounce. Best-effort.
29
+ */
30
+ clearPriorSessionHint: () => Promise<void>;
23
31
  switchSession: (sessionId: string) => Promise<User>;
24
32
  applyLanguagePreference: (user: User) => Promise<void>;
25
33
  onAuthStateChange?: (user: User | null) => void;
@@ -44,6 +52,24 @@ const LOGIN_ERROR_CODE = 'LOGIN_ERROR';
44
52
  const LOGOUT_ERROR_CODE = 'LOGOUT_ERROR';
45
53
  const LOGOUT_ALL_ERROR_CODE = 'LOGOUT_ALL_ERROR';
46
54
 
55
+ /**
56
+ * Fire-and-forget the durable returning-user hint clear on explicit sign-out.
57
+ *
58
+ * Mirrors the synchronous, non-blocking nature of the sibling
59
+ * `clearSsoBounceState()`: sign-out must NEVER block on (or fail because of) a
60
+ * best-effort storage write. The clear is invoked synchronously (so unit tests
61
+ * can assert it ran) but its async settle is detached; any rejection is logged,
62
+ * never thrown.
63
+ */
64
+ function clearPriorSessionHintSafe(
65
+ clearPriorSessionHint: () => Promise<void>,
66
+ logger?: (message: string, error?: unknown) => void,
67
+ ): void {
68
+ clearPriorSessionHint().catch((hintError) => {
69
+ logger?.('Failed to clear prior-session hint on sign-out', hintError);
70
+ });
71
+ }
72
+
47
73
  /**
48
74
  * Authentication operations using public key cryptography.
49
75
  * Accepts public key as parameter - identity management is handled by the app layer.
@@ -56,6 +82,7 @@ export const useAuthOperations = ({
56
82
  updateSessions,
57
83
  saveActiveSessionId,
58
84
  clearSessionState,
85
+ clearPriorSessionHint,
59
86
  switchSession,
60
87
  applyLanguagePreference,
61
88
  onAuthStateChange,
@@ -227,6 +254,7 @@ export const useAuthOperations = ({
227
254
  // Genuine FULL sign-out (no sessions remain): clear the per-origin
228
255
  // SSO bounce state so a fresh deliberate sign-in can re-probe.
229
256
  clearSsoBounceState();
257
+ clearPriorSessionHintSafe(clearPriorSessionHint, logger);
230
258
  await clearSessionState();
231
259
  return;
232
260
  }
@@ -237,6 +265,7 @@ export const useAuthOperations = ({
237
265
  if (isInvalid && targetSessionId === activeSessionId) {
238
266
  // The active session is invalid → full sign-out; clear SSO state too.
239
267
  clearSsoBounceState();
268
+ clearPriorSessionHintSafe(clearPriorSessionHint, logger);
240
269
  await clearSessionState();
241
270
  return;
242
271
  }
@@ -254,6 +283,7 @@ export const useAuthOperations = ({
254
283
  [
255
284
  activeSessionId,
256
285
  clearSessionState,
286
+ clearPriorSessionHint,
257
287
  logger,
258
288
  onError,
259
289
  oxyServices,
@@ -286,8 +316,11 @@ export const useAuthOperations = ({
286
316
  clearActiveAuthuser();
287
317
  }
288
318
  // logoutAll is ALWAYS a full sign-out: clear the per-origin SSO bounce
289
- // state (web-guarded internally) so a fresh sign-in can re-probe.
319
+ // state (web-guarded internally) so a fresh sign-in can re-probe, and drop
320
+ // the durable returning-user hint so the next cold boot is treated as a
321
+ // first-time anonymous visitor (no forced `/sso` bounce after sign-out).
290
322
  clearSsoBounceState();
323
+ clearPriorSessionHintSafe(clearPriorSessionHint, logger);
291
324
  await clearSessionState();
292
325
  } catch (error) {
293
326
  handleAuthError(error, {
@@ -299,7 +332,7 @@ export const useAuthOperations = ({
299
332
  });
300
333
  throw error instanceof Error ? error : new Error('Logout all failed');
301
334
  }
302
- }, [activeSessionId, clearSessionState, logger, onError, oxyServices, setAuthState]);
335
+ }, [activeSessionId, clearSessionState, clearPriorSessionHint, logger, onError, oxyServices, setAuthState]);
303
336
 
304
337
  return {
305
338
  signIn,
package/src/ui/index.ts CHANGED
@@ -12,6 +12,8 @@
12
12
  * cannot contain `require()` per CLAUDE.md) and (b) defeated tree-shaking.
13
13
  */
14
14
 
15
+ /// <reference path="../types/react-native-classname.d.ts" />
16
+
15
17
  // Components
16
18
  export { default as OxyProvider } from './components/OxyProvider';
17
19
  export { default as OxySignInButton } from './components/OxySignInButton';
@@ -63,21 +63,4 @@ export interface OxyProviderProps {
63
63
  authWebUrl?: string;
64
64
  authRedirectUri?: string;
65
65
  queryClient?: QueryClient;
66
- /**
67
- * When `true`, skips ONLY the terminal SSO bounce in the web cold-boot
68
- * chain — the force-redirect to `auth.<apex>/sso?prompt=none` that fires
69
- * for a visitor with no recoverable local session. This lets a truly
70
- * anonymous user keep browsing instead of being bounced to the central
71
- * IdP (e.g. a marketplace that allows anonymous browsing like eBay /
72
- * Shop.app).
73
- *
74
- * Session restore still runs in full: the callback consume, FedCM silent,
75
- * first-party `/auth/silent` iframe, stored-session bearer, and
76
- * cookie-restore steps all execute — so a returning signed-in user is
77
- * still silently restored. Only the force-bounce for a genuinely
78
- * anonymous visitor is suppressed.
79
- *
80
- * Default `false` (current behavior: the bounce fires).
81
- */
82
- disableAutoSso?: boolean;
83
66
  }
@@ -9,6 +9,17 @@ export interface SessionStorageKeys {
9
9
  activeSessionId: string;
10
10
  sessionIds: string;
11
11
  language: string;
12
+ /**
13
+ * DURABLE "this device/app has had a signed-in Oxy session before" hint.
14
+ *
15
+ * Lives in the SAME `storageKeyPrefix`-scoped durable store as the stored
16
+ * session ids, so it survives a session expiring (cleared only on explicit
17
+ * full sign-out). Read at cold boot to drive the smart `sso-bounce` gate:
18
+ * a returning visitor still gets ONE establish bounce so a central-only
19
+ * cross-domain session recovers, while a first-time anonymous visitor is
20
+ * never force-redirected. See `allowSsoBounce` in `@oxyhq/core`.
21
+ */
22
+ priorSession: string;
12
23
  }
13
24
 
14
25
  /**
@@ -125,6 +136,7 @@ export const getStorageKeys = (prefix: string = STORAGE_KEY_PREFIX): SessionStor
125
136
  activeSessionId: `${prefix}_active_session_id`,
126
137
  sessionIds: `${prefix}_session_ids`,
127
138
  language: `${prefix}_language`,
139
+ priorSession: `${prefix}_prior_session`,
128
140
  });
129
141
 
130
142