@oxy.so/services 11.0.0 → 11.2.0-oxy-one.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 (145) hide show
  1. package/lib/commonjs/index.js +13 -0
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/boot/runProviderColdBoot.js +14 -1
  4. package/lib/commonjs/ui/boot/runProviderColdBoot.js.map +1 -1
  5. package/lib/commonjs/ui/components/OxySignInButton.js +25 -7
  6. package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
  7. package/lib/commonjs/ui/context/OxyContext.js +198 -41
  8. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  9. package/lib/commonjs/ui/context/commitSessionFlow.js +5 -0
  10. package/lib/commonjs/ui/context/commitSessionFlow.js.map +1 -1
  11. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +56 -9
  12. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  13. package/lib/commonjs/ui/context/isolatedOAuthSession.js +52 -0
  14. package/lib/commonjs/ui/context/isolatedOAuthSession.js.map +1 -0
  15. package/lib/commonjs/ui/context/useOxyAccountGraph.js +7 -5
  16. package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/queries/usePersonalPlans.js +43 -0
  18. package/lib/commonjs/ui/hooks/queries/usePersonalPlans.js.map +1 -0
  19. package/lib/commonjs/ui/hooks/useAuth.js +7 -3
  20. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  21. package/lib/commonjs/ui/oauth/applicationSessionLane.js +32 -0
  22. package/lib/commonjs/ui/oauth/applicationSessionLane.js.map +1 -0
  23. package/lib/commonjs/ui/oauth/completeOAuthCode.js +4 -2
  24. package/lib/commonjs/ui/oauth/completeOAuthCode.js.map +1 -1
  25. package/lib/commonjs/ui/oauth/nativeAuthTransport.js +119 -0
  26. package/lib/commonjs/ui/oauth/nativeAuthTransport.js.map +1 -0
  27. package/lib/commonjs/ui/session/createSessionClient.js +14 -2
  28. package/lib/commonjs/ui/session/createSessionClient.js.map +1 -1
  29. package/lib/commonjs/ui/session/deviceCredentialTracker.js +8 -0
  30. package/lib/commonjs/ui/session/deviceCredentialTracker.js.map +1 -1
  31. package/lib/module/index.js +1 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/ui/boot/runProviderColdBoot.js +14 -1
  34. package/lib/module/ui/boot/runProviderColdBoot.js.map +1 -1
  35. package/lib/module/ui/components/OxySignInButton.js +25 -7
  36. package/lib/module/ui/components/OxySignInButton.js.map +1 -1
  37. package/lib/module/ui/context/OxyContext.js +200 -43
  38. package/lib/module/ui/context/OxyContext.js.map +1 -1
  39. package/lib/module/ui/context/commitSessionFlow.js +5 -0
  40. package/lib/module/ui/context/commitSessionFlow.js.map +1 -1
  41. package/lib/module/ui/context/hooks/useAuthOperations.js +56 -9
  42. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  43. package/lib/module/ui/context/isolatedOAuthSession.js +44 -0
  44. package/lib/module/ui/context/isolatedOAuthSession.js.map +1 -0
  45. package/lib/module/ui/context/useOxyAccountGraph.js +7 -5
  46. package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
  47. package/lib/module/ui/hooks/queries/usePersonalPlans.js +39 -0
  48. package/lib/module/ui/hooks/queries/usePersonalPlans.js.map +1 -0
  49. package/lib/module/ui/hooks/useAuth.js +7 -3
  50. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  51. package/lib/module/ui/oauth/applicationSessionLane.js +26 -0
  52. package/lib/module/ui/oauth/applicationSessionLane.js.map +1 -0
  53. package/lib/module/ui/oauth/completeOAuthCode.js +4 -2
  54. package/lib/module/ui/oauth/completeOAuthCode.js.map +1 -1
  55. package/lib/module/ui/oauth/nativeAuthTransport.js +115 -0
  56. package/lib/module/ui/oauth/nativeAuthTransport.js.map +1 -0
  57. package/lib/module/ui/session/createSessionClient.js +14 -2
  58. package/lib/module/ui/session/createSessionClient.js.map +1 -1
  59. package/lib/module/ui/session/deviceCredentialTracker.js +8 -0
  60. package/lib/module/ui/session/deviceCredentialTracker.js.map +1 -1
  61. package/lib/typescript/commonjs/index.d.ts +2 -0
  62. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts +7 -0
  64. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/components/OxySignInButton.d.ts +2 -0
  66. package/lib/typescript/commonjs/ui/components/OxySignInButton.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +3 -1
  68. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/context/commitSessionFlow.d.ts +2 -0
  70. package/lib/typescript/commonjs/ui/context/commitSessionFlow.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +3 -2
  72. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/ui/context/isolatedOAuthSession.d.ts +19 -0
  74. package/lib/typescript/commonjs/ui/context/isolatedOAuthSession.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +3 -0
  76. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts +2 -1
  78. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +6 -0
  80. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/ui/hooks/queries/usePersonalPlans.d.ts +64 -0
  82. package/lib/typescript/commonjs/ui/hooks/queries/usePersonalPlans.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/ui/oauth/applicationSessionLane.d.ts +6 -0
  85. package/lib/typescript/commonjs/ui/oauth/applicationSessionLane.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/ui/oauth/completeOAuthCode.d.ts +2 -2
  87. package/lib/typescript/commonjs/ui/oauth/nativeAuthTransport.d.ts +28 -0
  88. package/lib/typescript/commonjs/ui/oauth/nativeAuthTransport.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +1 -1
  90. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/ui/session/deviceCredentialTracker.d.ts.map +1 -1
  92. package/lib/typescript/module/index.d.ts +2 -0
  93. package/lib/typescript/module/index.d.ts.map +1 -1
  94. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts +7 -0
  95. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  96. package/lib/typescript/module/ui/components/OxySignInButton.d.ts +2 -0
  97. package/lib/typescript/module/ui/components/OxySignInButton.d.ts.map +1 -1
  98. package/lib/typescript/module/ui/context/OxyContext.d.ts +3 -1
  99. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  100. package/lib/typescript/module/ui/context/commitSessionFlow.d.ts +2 -0
  101. package/lib/typescript/module/ui/context/commitSessionFlow.d.ts.map +1 -1
  102. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +3 -2
  103. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  104. package/lib/typescript/module/ui/context/isolatedOAuthSession.d.ts +19 -0
  105. package/lib/typescript/module/ui/context/isolatedOAuthSession.d.ts.map +1 -0
  106. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +3 -0
  107. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
  108. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts +2 -1
  109. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  110. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +6 -0
  111. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  112. package/lib/typescript/module/ui/hooks/queries/usePersonalPlans.d.ts +64 -0
  113. package/lib/typescript/module/ui/hooks/queries/usePersonalPlans.d.ts.map +1 -0
  114. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  115. package/lib/typescript/module/ui/oauth/applicationSessionLane.d.ts +6 -0
  116. package/lib/typescript/module/ui/oauth/applicationSessionLane.d.ts.map +1 -0
  117. package/lib/typescript/module/ui/oauth/completeOAuthCode.d.ts +2 -2
  118. package/lib/typescript/module/ui/oauth/nativeAuthTransport.d.ts +28 -0
  119. package/lib/typescript/module/ui/oauth/nativeAuthTransport.d.ts.map +1 -0
  120. package/lib/typescript/module/ui/session/createSessionClient.d.ts +1 -1
  121. package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -1
  122. package/lib/typescript/module/ui/session/deviceCredentialTracker.d.ts.map +1 -1
  123. package/package.json +30 -30
  124. package/src/index.ts +4 -0
  125. package/src/ui/boot/__tests__/coldBootNoIdpRedirect.test.ts +56 -2
  126. package/src/ui/boot/runProviderColdBoot.ts +22 -2
  127. package/src/ui/components/OxySignInButton.tsx +20 -4
  128. package/src/ui/context/OxyContext.tsx +171 -40
  129. package/src/ui/context/__tests__/commitSessionFlow.test.ts +13 -0
  130. package/src/ui/context/__tests__/isolatedOAuthSession.test.ts +35 -0
  131. package/src/ui/context/commitSessionFlow.ts +8 -0
  132. package/src/ui/context/hooks/useAuthOperations.ts +55 -9
  133. package/src/ui/context/isolatedOAuthSession.ts +48 -0
  134. package/src/ui/context/oxyContextTypes.ts +4 -0
  135. package/src/ui/context/useOxyAccountGraph.ts +8 -5
  136. package/src/ui/hooks/queries/usePersonalPlans.ts +23 -0
  137. package/src/ui/hooks/useAuth.ts +6 -2
  138. package/src/ui/oauth/__tests__/completeOAuthCode.test.ts +1 -1
  139. package/src/ui/oauth/__tests__/nativeAuthTransport.test.ts +57 -0
  140. package/src/ui/oauth/__tests__/oauthBearerCommit.http.test.ts +131 -0
  141. package/src/ui/oauth/applicationSessionLane.ts +26 -0
  142. package/src/ui/oauth/completeOAuthCode.ts +3 -3
  143. package/src/ui/oauth/nativeAuthTransport.ts +67 -0
  144. package/src/ui/session/createSessionClient.ts +13 -1
  145. package/src/ui/session/deviceCredentialTracker.ts +7 -1
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.useOxyAuthActions = exports.useOxy = exports.useOptionalOxy = exports.OxyRuntimeProvider = exports.OxyProviderMissingError = void 0;
7
+ var _nativeAuthTransport = require("../oauth/nativeAuthTransport.js");
8
+ var _isolatedOAuthSession = require("./isolatedOAuthSession.js");
7
9
  var _react = require("react");
8
10
  var _reactNative = require("react-native");
9
11
  var _core = require("@oxy.so/core");
@@ -13,11 +15,14 @@ var _accountDialogManager = require("../navigation/accountDialogManager.js");
13
15
  var _oauthNavigation = require("../components/oauthNavigation.js");
14
16
  var _browserAuthTransport = require("../oauth/browserAuthTransport.js");
15
17
  var _browserBridge = require("../oauth/browserBridge.js");
18
+ var _sharedDeviceCredentialStore = require("../session/sharedDeviceCredentialStore.js");
16
19
  var _deviceCredentialTracker = require("../session/deviceCredentialTracker.js");
17
20
  var _explicitOAuthConsent = require("../oauth/explicitOAuthConsent.js");
18
21
  var _isWebBrowser = require("../utils/isWebBrowser.js");
19
22
  var _deliveryPlatform = require("../utils/deliveryPlatform.js");
20
23
  var _runProviderColdBoot = require("../boot/runProviderColdBoot.js");
24
+ var _applicationSessionLane = require("../oauth/applicationSessionLane.js");
25
+ var _errorHandlers = require("../utils/errorHandlers.js");
21
26
  var _deviceCredential = require("../utils/deviceCredential.js");
22
27
  var _tokenLossRecovery = require("../session/tokenLossRecovery.js");
23
28
  var _authStore = require("../stores/authStore.js");
@@ -139,9 +144,25 @@ const OxyRuntimeProvider = ({
139
144
  // Server-authoritative device session client. Built ONCE per `oxyServices`
140
145
  // instance. Device-scoped sockets connect with deviceId+deviceSecret when no
141
146
  // bearer is planted yet, so cross-origin apps sync via `session_state`.
147
+ const runtimeRef = (0, _react.useRef)(null);
148
+ const registeredLaneRef = (0, _react.useRef)({
149
+ clientId: clientIdProp,
150
+ services: oxyServices,
151
+ lane: null,
152
+ pending: null
153
+ });
154
+ if (registeredLaneRef.current.clientId !== clientIdProp || registeredLaneRef.current.services !== oxyServices) {
155
+ registeredLaneRef.current = {
156
+ clientId: clientIdProp,
157
+ services: oxyServices,
158
+ lane: null,
159
+ pending: null
160
+ };
161
+ }
142
162
  const sessionClientPairRef = (0, _react.useRef)(null);
143
163
  if (!sessionClientPairRef.current) {
144
164
  sessionClientPairRef.current = (0, _index2.createSessionClient)(oxyServices, origin => {
165
+ if (runtimeRef.current && (0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtimeRef.current)) return;
145
166
  // Erase the DURABLE device credential only on a `request`-origin verdict
146
167
  // (a REST sign-out / revocation). A `push`-origin empty state is a socket
147
168
  // broadcast that may be a transient reconnect artifact — clearing the
@@ -159,7 +180,10 @@ const OxyRuntimeProvider = ({
159
180
  // option — and the pinned account id once an identity-bound provider has
160
181
  // resolved its pin. Read through the ref so the resolver stays stable for
161
182
  // the client's lifetime while still seeing later resolutions.
162
- () => identityRef.current?.getPinnedAccountId() ?? null);
183
+ () => identityRef.current?.getPinnedAccountId() ?? null, () => registeredLaneRef.current.lane === 'device' && (!runtimeRef.current || !(0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtimeRef.current)), async () => {
184
+ if (identityRef.current || registeredLaneRef.current.lane !== 'device') return;
185
+ if ((await authStore.setAutomaticIdentitySignInSuppressed?.(true)) === false) throw new Error('Failed to persist explicit sign-out intent');
186
+ });
163
187
  }
164
188
  const {
165
189
  client: sessionClient,
@@ -179,7 +203,6 @@ const OxyRuntimeProvider = ({
179
203
  const saveSessionIdsRef = (0, _react.useRef)(() => undefined);
180
204
  const subjectChangeRef = (0, _react.useRef)(() => undefined);
181
205
  const reestablishRef = (0, _react.useRef)(() => undefined);
182
- const runtimeRef = (0, _react.useRef)(null);
183
206
  if (!runtimeRef.current) {
184
207
  runtimeRef.current = (0, _index.createOxyRuntime)({
185
208
  oxyServices,
@@ -394,11 +417,56 @@ const OxyRuntimeProvider = ({
394
417
  const cred = await (0, _deviceCredential.loadPersistedDeviceCredential)(authStore);
395
418
  sessionClientHost.setDeviceCredential(cred);
396
419
  }, [authStore, sessionClientHost]);
420
+
421
+ // Provider-owned classification: never accept a caller/button's trust verdict.
422
+ // Recovery asks freshly; concurrent boot/recovery requests share one lookup.
423
+ const resolveRegisteredLane = (0, _react.useCallback)(async () => {
424
+ const selected = registeredLaneRef.current;
425
+ if (selected.clientId !== clientIdProp || selected.services !== oxyServices) throw new Error('Provider classification was superseded');
426
+ if (selected.pending) return selected.pending;
427
+ selected.lane = null;
428
+ const pending = (async () => {
429
+ const lane = await (0, _applicationSessionLane.resolveApplicationSessionLane)(oxyServices, clientIdProp);
430
+ if (registeredLaneRef.current !== selected) throw new Error('Provider classification was superseded');
431
+ if (lane === 'oauth' && !(0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime)) {
432
+ sessionClient.resetLocalState();
433
+ sessionClientHost.setDeviceCredential(null);
434
+ await clearSessionStateRef.current();
435
+ }
436
+ if (registeredLaneRef.current !== selected) throw new Error('Provider classification was superseded');
437
+ selected.lane = lane;
438
+ return lane;
439
+ })();
440
+ selected.pending = pending;
441
+ try {
442
+ return await pending;
443
+ } finally {
444
+ if (selected.pending === pending) selected.pending = null;
445
+ }
446
+ }, [oxyServices, clientIdProp, runtime, sessionClient, sessionClientHost]);
447
+ const ensureDeviceSessionLane = (0, _react.useCallback)(() => {
448
+ if (registeredLaneRef.current.clientId !== clientIdProp || registeredLaneRef.current.services !== oxyServices) {
449
+ throw new Error('Provider classification was superseded');
450
+ }
451
+ if (registeredLaneRef.current.lane === 'oauth' || (0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime)) {
452
+ throw new Error('This application requires explicit OAuth sign-in');
453
+ }
454
+ if (registeredLaneRef.current.lane === 'device') return;
455
+ return resolveRegisteredLane().then(lane => {
456
+ if (registeredLaneRef.current.clientId !== clientIdProp || registeredLaneRef.current.services !== oxyServices) {
457
+ throw new Error('Provider classification was superseded');
458
+ }
459
+ if (lane !== 'device' || (0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime)) {
460
+ throw new Error('This application requires explicit OAuth sign-in');
461
+ }
462
+ });
463
+ }, [resolveRegisteredLane, runtime, clientIdProp, oxyServices]);
397
464
  const {
398
465
  signIn,
399
466
  logout,
400
467
  logoutAll
401
468
  } = (0, _useAuthOperations.useAuthOperations)({
469
+ ensureDeviceSessionLane,
402
470
  oxyServices,
403
471
  store: authStore,
404
472
  storage,
@@ -481,8 +549,9 @@ const OxyRuntimeProvider = ({
481
549
  };
482
550
  const recovery = (0, _tokenLossRecovery.createTokenLossRecovery)({
483
551
  remint: () => oxyServices.http.refreshAccessToken('preflight'),
484
- hasDeviceCredential: () => (0, _deviceCredential.hasPersistedSessionCredential)(authStore),
552
+ hasDeviceCredential: () => registeredLaneRef.current.lane !== 'device' || (0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime) ? Promise.resolve(false) : (0, _deviceCredential.hasPersistedSessionCredential)(authStore),
485
553
  hasKeyedRecovery: async () => {
554
+ if (registeredLaneRef.current.lane !== 'device' || (0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime)) return false;
486
555
  if (_reactNative.Platform.OS === 'web') {
487
556
  return false;
488
557
  }
@@ -533,19 +602,27 @@ const OxyRuntimeProvider = ({
533
602
  // account instead of the device's active one, and its recovery arm becomes the
534
603
  // PRIMARY-key identity sign-in rather than the cross-app shared keychain.
535
604
  (0, _react.useEffect)(() => {
536
- const dispose = (0, _session.installAuthRefreshHandler)({
605
+ let disposed = false;
606
+ const refresh = (0, _session.createAuthRefreshHandler)({
537
607
  oxy: oxyServices,
538
608
  store: authStore,
609
+ isCurrent: () => !disposed && registeredLaneRef.current.lane === 'device' && !(0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime),
539
610
  ...(identity ? {
540
611
  identity: identity.binding
612
+ } : {}),
613
+ ...(!identity && !(0, _isWebBrowser.isWebBrowser)() ? {
614
+ allowCommonsIdentityFallback: false,
615
+ sharedDeviceCredential: (0, _sharedDeviceCredentialStore.createPlatformSharedDeviceCredentialStore)() ?? undefined
541
616
  } : {})
542
617
  });
618
+ oxyServices.http.setAuthRefreshHandler(reason => registeredLaneRef.current.lane !== 'device' || (0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime) ? Promise.resolve(null) : refresh(reason));
543
619
  const scheduler = (0, _session.startTokenRefreshScheduler)(oxyServices);
544
620
  return () => {
621
+ disposed = true;
545
622
  scheduler.dispose();
546
- dispose();
623
+ oxyServices.http.setAuthRefreshHandler(null);
547
624
  };
548
- }, [oxyServices, authStore, identity]);
625
+ }, [oxyServices, authStore, identity, runtime]);
549
626
 
550
627
  // ── Session commit funnel ────────────────────────────────────────────────
551
628
  // The single place a session becomes committed: plant tokens, persist the
@@ -554,6 +631,22 @@ const OxyRuntimeProvider = ({
554
631
  // Used by the QR device flow (`handleWebSession`), password sign-in, 2FA
555
632
  // completion, and the cold boot.
556
633
  const commitSession = (0, _react.useCallback)(async (input, options) => {
634
+ const isolatedOAuth = options.oauth === true && !input.deviceId && !input.deviceSecret;
635
+ if (isolatedOAuth || (0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime)) {
636
+ sessionClient.resetLocalState();
637
+ sessionClientHost.setDeviceCredential(null);
638
+ await authStore.clear();
639
+ await clearSessionStateRef.current();
640
+ }
641
+ if (isolatedOAuth) {
642
+ if (!clientId) throw new Error('An isolated OAuth commit requires its registered client ID');
643
+ (0, _isolatedOAuthSession.setIsolatedOAuthSession)(runtime, {
644
+ sessionId: input.sessionId,
645
+ clientId
646
+ });
647
+ } else {
648
+ (0, _isolatedOAuthSession.clearIsolatedOAuthSession)(runtime);
649
+ }
557
650
  if (input.accessToken) {
558
651
  oxyServices.session.setAccessToken(input.accessToken);
559
652
  }
@@ -603,7 +696,7 @@ const OxyRuntimeProvider = ({
603
696
  userId: input.userId,
604
697
  isCurrent: true
605
698
  }], {
606
- merge: true
699
+ merge: !isolatedOAuth
607
700
  });
608
701
  runtime.setActiveSessionId(input.sessionId);
609
702
  });
@@ -619,6 +712,7 @@ const OxyRuntimeProvider = ({
619
712
  // on the next load).
620
713
  await (0, _commitSessionFlow.commitDeviceSetAndResolve)({
621
714
  activate: options.activate,
715
+ isolatedOAuth,
622
716
  hasDeviceState: input.deviceState !== undefined,
623
717
  userId: input.userId,
624
718
  fallbackUser: input.user ?? null,
@@ -631,7 +725,10 @@ const OxyRuntimeProvider = ({
631
725
  onAuthStateChange: onAuthStateChangeRef.current,
632
726
  markAuthResolved: markAuthResolvedRef.current
633
727
  });
634
- }, [oxyServices, authStore, runtime, sessionClient, sessionClientHost, syncFromClient, logger]);
728
+ if (options.activate && !isolatedOAuth && !identity && input.userId && runtime.getSnapshot().account?.id === input.userId) {
729
+ if ((await authStore.setAutomaticIdentitySignInSuppressed?.(false)) === false) logger('Failed to release explicit sign-out intent after sign-in');
730
+ }
731
+ }, [oxyServices, authStore, runtime, sessionClient, sessionClientHost, syncFromClient, logger, clientId, identity]);
635
732
  const commitSessionRef = (0, _react.useRef)(commitSession);
636
733
  commitSessionRef.current = commitSession;
637
734
 
@@ -666,9 +763,21 @@ const OxyRuntimeProvider = ({
666
763
  authorizeBaseUrl,
667
764
  identityBound: isIdentityBound,
668
765
  commitSession: input => commitSessionRef.current(input, {
669
- activate: true
766
+ activate: true,
767
+ oauth: true
670
768
  })
671
769
  }, options), [webAuthMode, oxyServices, clientId, authorizeBaseUrl, isIdentityBound]);
770
+ const startNativeOAuthSignInForContext = (0, _react.useCallback)(options => (0, _nativeAuthTransport.startNativeOAuthSignIn)({
771
+ platform: _reactNative.Platform.OS === 'ios' || _reactNative.Platform.OS === 'android' ? 'native' : 'unsupported',
772
+ oxyServices,
773
+ clientId,
774
+ authorizeBaseUrl,
775
+ identityBound: isIdentityBound,
776
+ commitSession: input => commitSessionRef.current(input, {
777
+ activate: true,
778
+ oauth: true
779
+ })
780
+ }, options), [oxyServices, clientId, authorizeBaseUrl, isIdentityBound]);
672
781
  const requestOAuthConsentForContext = (0, _react.useCallback)(options => (0, _explicitOAuthConsent.requestOAuthConsent)({
673
782
  platform: _reactNative.Platform.OS === 'web' ? 'web' : _reactNative.Platform.OS === 'ios' || _reactNative.Platform.OS === 'android' ? 'native' : 'unsupported',
674
783
  mode: webAuthMode,
@@ -678,7 +787,8 @@ const OxyRuntimeProvider = ({
678
787
  identityBound: isIdentityBound,
679
788
  expectedUserId: user?.id ?? null,
680
789
  commitSession: input => commitSessionRef.current(input, {
681
- activate: true
790
+ activate: true,
791
+ oauth: true
682
792
  })
683
793
  }, options), [webAuthMode, oxyServices, clientId, authorizeBaseUrl, isIdentityBound, user?.id]);
684
794
 
@@ -766,8 +876,8 @@ const OxyRuntimeProvider = ({
766
876
  // Native is untouched: its apps already share a device through the keychain.
767
877
  (0, _react.useEffect)(() => {
768
878
  if (!(0, _isWebBrowser.isWebBrowser)()) return undefined;
769
- return oxyServices.session.setDeviceCredentialProvider(() => (0, _deviceCredential.loadPersistedDeviceCredential)(authStore));
770
- }, [oxyServices, authStore]);
879
+ return oxyServices.session.setDeviceCredentialProvider(() => registeredLaneRef.current.lane === 'device' && !(0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime) ? (0, _deviceCredential.loadPersistedDeviceCredential)(authStore) : Promise.resolve(null));
880
+ }, [oxyServices, authStore, runtime]);
771
881
 
772
882
  // ── The browser bridge (ADR 0029 D2) ──────────────────────────────────────
773
883
  // The first time a person presses sign-in in this app on the web and it holds
@@ -780,7 +890,7 @@ const OxyRuntimeProvider = ({
780
890
  const bridgeInFlightRef = (0, _react.useRef)(false);
781
891
  const closeAccountDialogRef = (0, _react.useRef)(() => undefined);
782
892
  const startBrowserBridge = (0, _react.useCallback)(() => {
783
- if (isIdentityBound || !clientId || !(0, _isWebBrowser.isWebBrowser)() || bridgeInFlightRef.current) return;
893
+ if (registeredLaneRef.current.lane !== 'device' || (0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime) || isIdentityBound || !clientId || !(0, _isWebBrowser.isWebBrowser)() || bridgeInFlightRef.current) return;
784
894
  if (authStoreRef.current?.heldDeviceCredential() !== null) return;
785
895
  const bridgeOrigin = (0, _browserBridge.resolveBridgeOrigin)(authorizeBaseUrl);
786
896
  const pageOrigin = globalThis.location?.origin;
@@ -835,8 +945,10 @@ const OxyRuntimeProvider = ({
835
945
  }).finally(() => {
836
946
  bridgeInFlightRef.current = false;
837
947
  });
838
- }, [isIdentityBound, clientId, authorizeBaseUrl, authRedirectUri, oxyServices, authStore, sessionClientHost, logger]);
839
- const openAccountDialog = (0, _react.useCallback)(view => {
948
+ }, [isIdentityBound, clientId, authorizeBaseUrl, authRedirectUri, oxyServices, authStore, sessionClientHost, logger, runtime]);
949
+ const openClassifiedAccountDialog = (0, _react.useCallback)(view => {
950
+ if (registeredLaneRef.current.clientId !== clientIdProp || registeredLaneRef.current.services !== oxyServices) return;
951
+ if (registeredLaneRef.current.lane !== 'device' || (0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime)) return;
840
952
  if (isIdentityBound) {
841
953
  // No controller was built, and there is nothing to choose: this app's user
842
954
  // is fixed to the owner of the local identity key.
@@ -899,7 +1011,24 @@ const OxyRuntimeProvider = ({
899
1011
  }
900
1012
  });
901
1013
  setAccountDialogOpen(true);
902
- }, [isIdentityBound, startBrowserBridge]);
1014
+ }, [isIdentityBound, startBrowserBridge, runtime, clientIdProp, oxyServices]);
1015
+ const openAccountDialog = (0, _react.useCallback)(view => {
1016
+ if (registeredLaneRef.current.clientId !== clientIdProp || registeredLaneRef.current.services !== oxyServices) return;
1017
+ if (registeredLaneRef.current.lane === 'device') {
1018
+ // Preserve the browser bridge's synchronous user-gesture path.
1019
+ openClassifiedAccountDialog(view);
1020
+ return;
1021
+ }
1022
+ void Promise.resolve().then(() => ensureDeviceSessionLane()).then(() => openClassifiedAccountDialog(view)).catch(error => {
1023
+ (0, _errorHandlers.handleAuthError)(error, {
1024
+ defaultMessage: 'Sign in failed',
1025
+ code: 'LOGIN_ERROR',
1026
+ onError,
1027
+ setAuthError: message => runtime.setError(message),
1028
+ logger
1029
+ });
1030
+ });
1031
+ }, [ensureDeviceSessionLane, openClassifiedAccountDialog, onError, runtime, logger, clientIdProp, oxyServices]);
903
1032
  const closeAccountDialog = (0, _react.useCallback)(() => {
904
1033
  accountDialogControllerRef.current?.cancelSignIn();
905
1034
  dismissAccountDialogSurface();
@@ -942,9 +1071,6 @@ const OxyRuntimeProvider = ({
942
1071
  // SYNCHRONOUS read on `SessionClient`'s apply path, and a stale `null` there
943
1072
  // would let a pinned client behave like an account-mode one for the first
944
1073
  // state it applies (including re-electing the device's active account).
945
- if (identity) {
946
- await identity.ensurePinnedAccountId();
947
- }
948
1074
  await (0, _runProviderColdBoot.runProviderColdBoot)({
949
1075
  oxyServices,
950
1076
  authStore,
@@ -952,6 +1078,13 @@ const OxyRuntimeProvider = ({
952
1078
  authRedirectUri,
953
1079
  sessionMode,
954
1080
  identity: identity?.binding,
1081
+ resolveSessionLane: resolveRegisteredLane,
1082
+ beforeDeviceBoot: identity ? async () => {
1083
+ await identity.ensurePinnedAccountId();
1084
+ } : undefined,
1085
+ afterDeviceBoot: identity ? async () => {
1086
+ await identity.refreshPinnedAccountId();
1087
+ } : undefined,
955
1088
  sessionClient,
956
1089
  syncDeviceCredentialToHost,
957
1090
  commitSession: (input, options) => commitSessionRef.current(input, options),
@@ -961,10 +1094,7 @@ const OxyRuntimeProvider = ({
961
1094
  // The boot may have (re-)established the identity session or cleared a pin
962
1095
  // whose key no longer exists, so re-resolve rather than trusting the memo
963
1096
  // taken before it ran.
964
- if (identity) {
965
- await identity.refreshPinnedAccountId();
966
- }
967
- }, [oxyServices, authStore, clientIdProp, authRedirectUri, sessionMode, identity, syncDeviceCredentialToHost, sessionClient, setTokenReady]);
1097
+ }, [resolveRegisteredLane, oxyServices, authStore, clientIdProp, authRedirectUri, sessionMode, identity, syncDeviceCredentialToHost, sessionClient, setTokenReady]);
968
1098
  (0, _react.useEffect)(() => {
969
1099
  if (initialized) {
970
1100
  return;
@@ -980,30 +1110,51 @@ const OxyRuntimeProvider = ({
980
1110
  });
981
1111
  }, [runColdBoot, storage, initialized, logger]);
982
1112
 
983
- // Reconcile device state when the tab returns to foreground (background tabs may
984
- // miss socket pushes or have stale bearer tokens).
1113
+ // Reconcile device state on foreground: suspended native apps and background
1114
+ // tabs may miss a sibling's active-context switch or socket pushes.
985
1115
  (0, _react.useEffect)(() => {
986
- if (!(0, _isWebBrowser.isWebBrowser)()) return;
987
- const onVisibility = () => {
988
- if (document.visibilityState !== 'visible') return;
989
- const reconcile = async () => {
990
- if (!oxyServices.session.accessToken && sessionClientHost.getDeviceCredential()) {
991
- // Route through the ONE shared single-flight the scheduler/preflight/401
992
- // use — never a private mint lane — so a tab-focus reconcile can't
993
- // double-rotate the device secret against them.
1116
+ let disposed = false;
1117
+ let pending = null;
1118
+ const reconcile = () => {
1119
+ if (disposed || pending || (0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime)) return;
1120
+ pending = (async () => {
1121
+ if (!(0, _isWebBrowser.isWebBrowser)() && !identity || !oxyServices.session.accessToken && sessionClientHost.getDeviceCredential()) {
1122
+ // The scheduler/preflight/401 paths own the same mint single-flight.
994
1123
  await oxyServices.http.refreshAccessToken('preflight');
995
1124
  }
1125
+ if (disposed || (0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime)) return;
1126
+ if (!(0, _isWebBrowser.isWebBrowser)() && !identity) await syncDeviceCredentialToHost();
996
1127
  if (!oxyServices.session.accessToken && !sessionClientHost.getDeviceCredential()) {
997
1128
  return;
998
1129
  }
999
1130
  await sessionClient.bootstrap();
1131
+ if (disposed || (0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime)) return;
1000
1132
  await syncFromClient();
1133
+ })().catch(() => undefined).finally(() => {
1134
+ pending = null;
1135
+ });
1136
+ };
1137
+ if ((0, _isWebBrowser.isWebBrowser)()) {
1138
+ const onVisibility = () => {
1139
+ if (document.visibilityState === 'visible') reconcile();
1140
+ };
1141
+ document.addEventListener('visibilitychange', onVisibility);
1142
+ return () => {
1143
+ disposed = true;
1144
+ document.removeEventListener('visibilitychange', onVisibility);
1001
1145
  };
1002
- void reconcile().catch(() => undefined);
1146
+ }
1147
+ let previousState = _reactNative.AppState.currentState;
1148
+ const subscription = _reactNative.AppState.addEventListener('change', state => {
1149
+ const resumed = state === 'active' && (previousState === 'inactive' || previousState === 'background');
1150
+ previousState = state;
1151
+ if (resumed) reconcile();
1152
+ });
1153
+ return () => {
1154
+ disposed = true;
1155
+ subscription.remove();
1003
1156
  };
1004
- document.addEventListener('visibilitychange', onVisibility);
1005
- return () => document.removeEventListener('visibilitychange', onVisibility);
1006
- }, [oxyServices, sessionClient, sessionClientHost, syncFromClient]);
1157
+ }, [oxyServices, sessionClient, sessionClientHost, syncFromClient, syncDeviceCredentialToHost, identity, runtime]);
1007
1158
 
1008
1159
  // Reconnect heal: when connectivity transitions offline→online while there is
1009
1160
  // no live access token but a persisted device credential exists, re-mint ONCE
@@ -1027,6 +1178,7 @@ const OxyRuntimeProvider = ({
1027
1178
  return;
1028
1179
  }
1029
1180
  void (async () => {
1181
+ if ((0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime)) return;
1030
1182
  if (oxyServices.session.accessToken || !sessionClientHost.getDeviceCredential()) {
1031
1183
  return;
1032
1184
  }
@@ -1038,14 +1190,15 @@ const OxyRuntimeProvider = ({
1038
1190
  await syncFromClient();
1039
1191
  })().catch(() => undefined);
1040
1192
  });
1041
- }, [oxyServices, sessionClient, sessionClientHost, syncFromClient]);
1193
+ }, [oxyServices, sessionClient, sessionClientHost, syncFromClient, runtime]);
1042
1194
 
1043
1195
  // Exposed `refreshSessions`: re-bootstrap the server-authoritative device
1044
1196
  // state and reproject — the manual counterpart to the realtime socket.
1045
1197
  const refreshSessionsForContext = (0, _react.useCallback)(async () => {
1198
+ if ((0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime)) throw new Error('An isolated OAuth session has no shared device sessions');
1046
1199
  await sessionClient.bootstrap();
1047
1200
  await syncFromClient();
1048
- }, [sessionClient, syncFromClient]);
1201
+ }, [sessionClient, syncFromClient, runtime]);
1049
1202
 
1050
1203
  // Exposed `switchSession`: route through the server-authoritative
1051
1204
  // `SessionClient`. Resolve the target account from the current device state,
@@ -1054,6 +1207,7 @@ const OxyRuntimeProvider = ({
1054
1207
  // Identity-bound providers reject: this is the session-keyed twin of
1055
1208
  // `switchToAccount`, so leaving it live would be a bypass around the same rule.
1056
1209
  const switchSessionForContext = (0, _react.useCallback)(async sessionId => {
1210
+ if ((0, _isolatedOAuthSession.hasIsolatedOAuthSession)(runtime)) throw new Error('An isolated OAuth session cannot switch device sessions');
1057
1211
  if (isIdentityBound) {
1058
1212
  throw new _index2.IdentityBoundSessionError('switchSession');
1059
1213
  }
@@ -1100,6 +1254,7 @@ const OxyRuntimeProvider = ({
1100
1254
  tokenReady,
1101
1255
  initialized,
1102
1256
  identityBound: isIdentityBound,
1257
+ isolatedOAuth: (0, _isolatedOAuthSession.readIsolatedOAuthSession)(runtime) !== null,
1103
1258
  oxyServices,
1104
1259
  sessionClient,
1105
1260
  syncFromClient,
@@ -1186,6 +1341,7 @@ const OxyRuntimeProvider = ({
1186
1341
  revokeSuspiciousSignIn,
1187
1342
  handleWebSession,
1188
1343
  startWebOAuthSignIn: startWebOAuthSignInForContext,
1344
+ startNativeOAuthSignIn: startNativeOAuthSignInForContext,
1189
1345
  requestOAuthConsent: requestOAuthConsentForContext,
1190
1346
  logout,
1191
1347
  logoutAll,
@@ -1213,8 +1369,9 @@ const OxyRuntimeProvider = ({
1213
1369
  switchToAccount,
1214
1370
  refreshAccounts,
1215
1371
  createAccount: createAccountFn
1216
- }), [user, sessions, activeSessionId, isAuthenticated, isLoading, tokenReady, hasAccessToken, canUsePrivateApi, isPrivateApiPending, authResolved, snapshot.storageReady, isAccountCacheReady, isIdentityBound, webAuthMode, error, currentLanguage, currentLanguages, currentLanguageMetadata, currentLanguageName, currentNativeLanguageName, hasIdentity, getPublicKey, signIn, revokeSuspiciousSignIn, handleWebSession, startWebOAuthSignInForContext, requestOAuthConsentForContext, logout, logoutAll, switchSessionForContext, refreshSessionsForContext, setLanguage, getDeviceSessions, logoutAllDeviceSessions, updateDeviceName, clearSessionState, clearAllAccountData, storageKeyPrefix, clientId, oxyServices, sessionClient, showBottomSheetForContext, openAvatarPicker, accountDialogController, accountDialogOpen, openAccountDialog, closeAccountDialog, accounts, switchToAccount, refreshAccounts, createAccountFn]);
1372
+ }), [user, sessions, activeSessionId, isAuthenticated, isLoading, tokenReady, hasAccessToken, canUsePrivateApi, isPrivateApiPending, authResolved, snapshot.storageReady, isAccountCacheReady, isIdentityBound, webAuthMode, error, currentLanguage, currentLanguages, currentLanguageMetadata, currentLanguageName, currentNativeLanguageName, hasIdentity, getPublicKey, signIn, revokeSuspiciousSignIn, handleWebSession, startWebOAuthSignInForContext, startNativeOAuthSignInForContext, requestOAuthConsentForContext, logout, logoutAll, switchSessionForContext, refreshSessionsForContext, setLanguage, getDeviceSessions, logoutAllDeviceSessions, updateDeviceName, clearSessionState, clearAllAccountData, storageKeyPrefix, clientId, oxyServices, sessionClient, showBottomSheetForContext, openAvatarPicker, accountDialogController, accountDialogOpen, openAccountDialog, closeAccountDialog, accounts, switchToAccount, refreshAccounts, createAccountFn]);
1217
1373
  const authActionsValue = (0, _react.useMemo)(() => ({
1374
+ ensureDeviceSessionLane,
1218
1375
  signIn,
1219
1376
  logout,
1220
1377
  logoutAll,
@@ -1226,7 +1383,7 @@ const OxyRuntimeProvider = ({
1226
1383
  showBottomSheet: showBottomSheetForContext,
1227
1384
  openAvatarPicker,
1228
1385
  openAccountDialog
1229
- }), [signIn, logout, logoutAll, refreshSessionsForContext, oxyServices, switchToAccount, hasIdentity, getPublicKey, showBottomSheetForContext, openAvatarPicker, openAccountDialog]);
1386
+ }), [ensureDeviceSessionLane, signIn, logout, logoutAll, refreshSessionsForContext, oxyServices, switchToAccount, hasIdentity, getPublicKey, showBottomSheetForContext, openAvatarPicker, openAccountDialog]);
1230
1387
 
1231
1388
  // Two contexts, deliberately. The inner one carries the wide compatibility
1232
1389
  // value every `useOxy()` consumer reads, rebuilt whenever any of its members