@oxyhq/services 13.3.0 → 14.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.
- package/lib/commonjs/index.js +11 -17
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/AccountMenu.js +103 -65
- package/lib/commonjs/ui/components/AccountMenu.js.map +1 -1
- package/lib/commonjs/ui/components/AccountSwitcher.js +84 -126
- package/lib/commonjs/ui/components/AccountSwitcher.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileMenu.js +140 -153
- package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
- package/lib/commonjs/ui/components/SignInModal.js +4 -4
- package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +506 -466
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +59 -57
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/context/inSessionTokenRefresh.js +23 -42
- package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +1 -1
- package/lib/commonjs/ui/context/silentSessionRestore.js +12 -21
- package/lib/commonjs/ui/context/silentSessionRestore.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +4 -3
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js +12 -7
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionManagement.js +17 -65
- package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +288 -0
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -0
- package/lib/commonjs/ui/hooks/useWebSSO.js +12 -191
- package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +2 -2
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/ui/session/createSessionClient.js +28 -0
- package/lib/commonjs/ui/session/createSessionClient.js.map +1 -0
- package/lib/commonjs/ui/session/index.js +52 -0
- package/lib/commonjs/ui/session/index.js.map +1 -0
- package/lib/commonjs/ui/session/projectSessionState.js +86 -0
- package/lib/commonjs/ui/session/projectSessionState.js.map +1 -0
- package/lib/commonjs/ui/session/sessionClientHost.js +30 -0
- package/lib/commonjs/ui/session/sessionClientHost.js.map +1 -0
- package/lib/commonjs/ui/session/tokenTransport.js +77 -0
- package/lib/commonjs/ui/session/tokenTransport.js.map +1 -0
- package/lib/commonjs/ui/utils/activeAuthuser.js +35 -79
- package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/commonjs/ui/utils/sessionHelpers.js +1 -51
- package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/commonjs/utils/deviceFlowSignIn.js +70 -15
- package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/module/index.js +5 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/AccountMenu.js +103 -65
- package/lib/module/ui/components/AccountMenu.js.map +1 -1
- package/lib/module/ui/components/AccountSwitcher.js +85 -127
- package/lib/module/ui/components/AccountSwitcher.js.map +1 -1
- package/lib/module/ui/components/ProfileMenu.js +143 -156
- package/lib/module/ui/components/ProfileMenu.js.map +1 -1
- package/lib/module/ui/components/SignInModal.js +4 -4
- package/lib/module/ui/components/SignInModal.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +510 -470
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +61 -60
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/context/inSessionTokenRefresh.js +24 -43
- package/lib/module/ui/context/inSessionTokenRefresh.js.map +1 -1
- package/lib/module/ui/context/silentSessionRestore.js +12 -20
- package/lib/module/ui/context/silentSessionRestore.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +4 -3
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/hooks/useOxyAuthSession.js +12 -7
- package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js +18 -66
- package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/hooks/useSwitchableAccounts.js +283 -0
- package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -0
- package/lib/module/ui/hooks/useWebSSO.js +13 -192
- package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/module/ui/index.js +0 -1
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +2 -2
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/session/createSessionClient.js +25 -0
- package/lib/module/ui/session/createSessionClient.js.map +1 -0
- package/lib/module/ui/session/index.js +17 -0
- package/lib/module/ui/session/index.js.map +1 -0
- package/lib/module/ui/session/projectSessionState.js +79 -0
- package/lib/module/ui/session/projectSessionState.js.map +1 -0
- package/lib/module/ui/session/sessionClientHost.js +26 -0
- package/lib/module/ui/session/sessionClientHost.js.map +1 -0
- package/lib/module/ui/session/tokenTransport.js +74 -0
- package/lib/module/ui/session/tokenTransport.js.map +1 -0
- package/lib/module/ui/utils/activeAuthuser.js +34 -76
- package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/module/ui/utils/sessionHelpers.js +0 -49
- package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/module/utils/deviceFlowSignIn.js +71 -16
- package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +4 -2
- package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +7 -8
- package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +12 -9
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +16 -13
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +27 -3
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +22 -17
- package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +12 -18
- package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +2 -2
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +162 -0
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +13 -52
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +15 -0
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/index.d.ts +15 -0
- package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +45 -0
- package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +14 -0
- package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +24 -0
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +21 -29
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts +0 -19
- package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +51 -19
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AccountMenu.d.ts +4 -2
- package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +7 -8
- package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts +12 -9
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +16 -13
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +27 -3
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +22 -17
- package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +12 -18
- package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +2 -2
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +162 -0
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +13 -52
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/createSessionClient.d.ts +15 -0
- package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/index.d.ts +15 -0
- package/lib/typescript/module/ui/session/index.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/projectSessionState.d.ts +45 -0
- package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/sessionClientHost.d.ts +14 -0
- package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/tokenTransport.d.ts +24 -0
- package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +21 -29
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/sessionHelpers.d.ts +0 -19
- package/lib/typescript/module/ui/utils/sessionHelpers.d.ts.map +1 -1
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +51 -19
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +9 -9
- package/src/ui/components/AccountMenu.tsx +117 -70
- package/src/ui/components/AccountSwitcher.tsx +88 -134
- package/src/ui/components/ProfileMenu.tsx +158 -162
- package/src/ui/components/SignInModal.tsx +5 -5
- package/src/ui/context/OxyContext.tsx +560 -475
- package/src/ui/context/hooks/useAuthOperations.ts +72 -63
- package/src/ui/context/inSessionTokenRefresh.ts +23 -42
- package/src/ui/context/silentSessionRestore.ts +12 -26
- package/src/ui/hooks/useAuth.ts +4 -3
- package/src/ui/hooks/useOxyAuthSession.ts +13 -8
- package/src/ui/hooks/useSessionManagement.ts +16 -87
- package/src/ui/hooks/useSwitchableAccounts.ts +397 -0
- package/src/ui/hooks/useWebSSO.ts +13 -227
- package/src/ui/index.ts +0 -1
- package/src/ui/screens/OxyAuthScreen.tsx +2 -2
- package/src/ui/session/__tests__/createSessionClient.test.ts +41 -0
- package/src/ui/session/__tests__/projectSessionState.test.ts +114 -0
- package/src/ui/session/__tests__/sessionClientHost.test.ts +43 -0
- package/src/ui/session/__tests__/tokenTransport.test.ts +142 -0
- package/src/ui/session/createSessionClient.ts +21 -0
- package/src/ui/session/index.ts +19 -0
- package/src/ui/session/projectSessionState.ts +85 -0
- package/src/ui/session/sessionClientHost.ts +27 -0
- package/src/ui/session/tokenTransport.ts +76 -0
- package/src/ui/utils/activeAuthuser.ts +34 -76
- package/src/ui/utils/sessionHelpers.ts +0 -66
- package/src/utils/__tests__/deviceFlowSignIn.test.ts +243 -34
- package/src/utils/deviceFlowSignIn.ts +98 -21
- package/lib/commonjs/ui/components/accountMenuRows.js +0 -33
- package/lib/commonjs/ui/components/accountMenuRows.js.map +0 -1
- package/lib/commonjs/ui/hooks/useDeviceAccounts.js +0 -280
- package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +0 -1
- package/lib/commonjs/ui/hooks/useSessionSocket.js +0 -203
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +0 -1
- package/lib/commonjs/utils/silentGuardKey.js +0 -54
- package/lib/commonjs/utils/silentGuardKey.js.map +0 -1
- package/lib/module/ui/components/accountMenuRows.js +0 -29
- package/lib/module/ui/components/accountMenuRows.js.map +0 -1
- package/lib/module/ui/hooks/useDeviceAccounts.js +0 -276
- package/lib/module/ui/hooks/useDeviceAccounts.js.map +0 -1
- package/lib/module/ui/hooks/useSessionSocket.js +0 -199
- package/lib/module/ui/hooks/useSessionSocket.js.map +0 -1
- package/lib/module/utils/silentGuardKey.js +0 -49
- package/lib/module/utils/silentGuardKey.js.map +0 -1
- package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts +0 -34
- package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts +0 -133
- package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts +0 -14
- package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts.map +0 -1
- package/lib/typescript/commonjs/utils/silentGuardKey.d.ts +0 -31
- package/lib/typescript/commonjs/utils/silentGuardKey.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/accountMenuRows.d.ts +0 -34
- package/lib/typescript/module/ui/components/accountMenuRows.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts +0 -133
- package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts +0 -14
- package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts.map +0 -1
- package/lib/typescript/module/utils/silentGuardKey.d.ts +0 -31
- package/lib/typescript/module/utils/silentGuardKey.d.ts.map +0 -1
- package/src/ui/components/accountMenuRows.ts +0 -48
- package/src/ui/hooks/useDeviceAccounts.ts +0 -344
- package/src/ui/hooks/useSessionSocket.ts +0 -233
- package/src/ui/utils/__tests__/activeAuthuser.test.ts +0 -75
- package/src/utils/__tests__/silentGuardKey.test.ts +0 -82
- package/src/utils/silentGuardKey.ts +0 -46
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Web-only
|
|
2
|
+
* Web-only helpers around the retired `oxy_rt_${authuser}` multi-slot
|
|
3
|
+
* refresh-cookie scheme, PLUS the (unrelated, still load-bearing) deliberate
|
|
4
|
+
* sign-out / SSO-bounce gates.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* It
|
|
14
|
-
*
|
|
6
|
+
* NOTE (session-sync cutover, Task 5): the device account SET is now
|
|
7
|
+
* server-authoritative via `SessionClient` (`@oxyhq/core`) — nothing in
|
|
8
|
+
* `@oxyhq/services` writes the persisted active-`authuser`-slot key anymore
|
|
9
|
+
* (`writeActiveAuthuser`/`clearActiveAuthuser` were deleted; their only
|
|
10
|
+
* callers were the deleted `establishDeviceRefreshSlot` sign-in registration
|
|
11
|
+
* and the deleted `switchToAccount`/`switchSession` slot bookkeeping).
|
|
12
|
+
* {@link readActiveAuthuser} is KEPT purely as an OPTIONAL restore hint:
|
|
13
|
+
* `OxyContext`'s `restoreStoredSession` cold-boot step reads it only to
|
|
14
|
+
* backfill `clientSession.authuser` for the active session when a legacy value
|
|
15
|
+
* happens to be present. It NO LONGER gates whether restore is attempted — the
|
|
16
|
+
* old guard that also required this marker made every web reload with a lapsed
|
|
17
|
+
* local bearer bail before any network validation (P0), and was replaced by a
|
|
18
|
+
* "bail only when there is nothing to restore" guard plus a valid-session
|
|
19
|
+
* election. Since nothing writes the key anymore, the backfill is dormant on
|
|
20
|
+
* fresh installs; it stays for legacy installs and costs nothing.
|
|
15
21
|
*
|
|
16
22
|
* Native (React Native) has no equivalent of these device-local cookies
|
|
17
23
|
* and uses bearer-protected session ids directly, so these helpers no-op
|
|
@@ -30,22 +36,8 @@ import {
|
|
|
30
36
|
|
|
31
37
|
const ACTIVE_AUTHUSER_KEY = 'oxy_active_authuser';
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
* unavailable. The PROPERTY ACCESS itself (`window.localStorage`) can throw a
|
|
36
|
-
* `SecurityError` synchronously in opaque-origin / sandboxed iframes or when
|
|
37
|
-
* storage is disabled — even `typeof window.localStorage` evaluates the getter
|
|
38
|
-
* and throws. Every read/write in this module routes through here so the getter
|
|
39
|
-
* throw is caught once, at the source, and callers stay clean. Returns `null`
|
|
40
|
-
* off-web and on any access failure (fail safe).
|
|
41
|
-
*/
|
|
42
|
-
function getLocalStorage(): Storage | null {
|
|
43
|
-
if (typeof window === 'undefined') return null;
|
|
44
|
-
try {
|
|
45
|
-
return window.localStorage ?? null;
|
|
46
|
-
} catch {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
39
|
+
function hasLocalStorage(): boolean {
|
|
40
|
+
return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
|
|
49
41
|
}
|
|
50
42
|
|
|
51
43
|
function getSessionStorage(): Storage | null {
|
|
@@ -65,12 +57,11 @@ function getSessionStorage(): Storage | null {
|
|
|
65
57
|
* fall back to deterministic selection (lowest authuser).
|
|
66
58
|
*/
|
|
67
59
|
export function readActiveAuthuser(): number | null {
|
|
68
|
-
|
|
69
|
-
if (!storage) {
|
|
60
|
+
if (!hasLocalStorage()) {
|
|
70
61
|
return null;
|
|
71
62
|
}
|
|
72
63
|
try {
|
|
73
|
-
const raw =
|
|
64
|
+
const raw = window.localStorage.getItem(ACTIVE_AUTHUSER_KEY);
|
|
74
65
|
if (raw === null) return null;
|
|
75
66
|
const parsed = Number.parseInt(raw, 10);
|
|
76
67
|
if (!Number.isFinite(parsed) || parsed < 0) return null;
|
|
@@ -80,50 +71,18 @@ export function readActiveAuthuser(): number | null {
|
|
|
80
71
|
}
|
|
81
72
|
}
|
|
82
73
|
|
|
83
|
-
/**
|
|
84
|
-
* Persist the active `authuser` slot index. No-ops on native and on any
|
|
85
|
-
* storage failure (e.g. Safari private mode). Callers MUST NOT depend on
|
|
86
|
-
* this succeeding — it is best-effort UX persistence, not authoritative.
|
|
87
|
-
*/
|
|
88
|
-
export function writeActiveAuthuser(authuser: number): void {
|
|
89
|
-
if (!Number.isFinite(authuser) || authuser < 0) return;
|
|
90
|
-
const storage = getLocalStorage();
|
|
91
|
-
if (!storage) return;
|
|
92
|
-
try {
|
|
93
|
-
storage.setItem(ACTIVE_AUTHUSER_KEY, String(authuser));
|
|
94
|
-
} catch {
|
|
95
|
-
// Best-effort persistence; swallow QuotaExceededError / SecurityError.
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Clear the persisted active `authuser` slot index. Called on full sign-out
|
|
101
|
-
* (logoutAll) so that the next cold boot doesn't try to resurrect a
|
|
102
|
-
* cleared slot.
|
|
103
|
-
*/
|
|
104
|
-
export function clearActiveAuthuser(): void {
|
|
105
|
-
const storage = getLocalStorage();
|
|
106
|
-
if (!storage) return;
|
|
107
|
-
try {
|
|
108
|
-
storage.removeItem(ACTIVE_AUTHUSER_KEY);
|
|
109
|
-
} catch {
|
|
110
|
-
// Best-effort.
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
74
|
/**
|
|
115
75
|
* Mark this origin as DELIBERATELY signed out (durable `localStorage`, via the
|
|
116
76
|
* core {@link ssoSignedOutKey}). Called ONLY on EXPLICIT full sign-out so that
|
|
117
77
|
* the next cold boot does NOT silently re-mint a session from a still-live IdP
|
|
118
|
-
* session (
|
|
78
|
+
* session (the per-apex `/auth/silent` iframe cold-boot step). Cleared by any
|
|
119
79
|
* deliberate sign-in (see {@link clearSignedOut}). No-ops on native / storage
|
|
120
80
|
* failure (best-effort).
|
|
121
81
|
*/
|
|
122
82
|
export function markSignedOut(): void {
|
|
123
|
-
|
|
124
|
-
if (!storage) return;
|
|
83
|
+
if (!hasLocalStorage()) return;
|
|
125
84
|
try {
|
|
126
|
-
|
|
85
|
+
window.localStorage.setItem(ssoSignedOutKey(window.location.origin), '1');
|
|
127
86
|
} catch {
|
|
128
87
|
// Best-effort; swallow QuotaExceededError / SecurityError (private mode).
|
|
129
88
|
}
|
|
@@ -131,15 +90,14 @@ export function markSignedOut(): void {
|
|
|
131
90
|
|
|
132
91
|
/**
|
|
133
92
|
* Clear the durable deliberately-signed-out flag. Called on ANY deliberate
|
|
134
|
-
* sign-in (password,
|
|
93
|
+
* sign-in (password, account switch, device claim) so a real sign-in
|
|
135
94
|
* fully re-enables automatic silent restore — there is no "stuck signed out"
|
|
136
95
|
* state. No-ops on native / storage failure.
|
|
137
96
|
*/
|
|
138
97
|
export function clearSignedOut(): void {
|
|
139
|
-
|
|
140
|
-
if (!storage) return;
|
|
98
|
+
if (!hasLocalStorage()) return;
|
|
141
99
|
try {
|
|
142
|
-
|
|
100
|
+
window.localStorage.removeItem(ssoSignedOutKey(window.location.origin));
|
|
143
101
|
} catch {
|
|
144
102
|
// Best-effort.
|
|
145
103
|
}
|
|
@@ -150,12 +108,14 @@ export function clearSignedOut(): void {
|
|
|
150
108
|
* the user deliberately signed out. Reads the durable flag through the core
|
|
151
109
|
* {@link silentRestoreSuppressed} predicate. Returns `false` off-web and on any
|
|
152
110
|
* storage failure (fail safe toward normal restore). Used to gate the
|
|
153
|
-
* `
|
|
111
|
+
* `silent-iframe` cold-boot step.
|
|
154
112
|
*/
|
|
155
113
|
export function isSilentRestoreSuppressed(): boolean {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
114
|
+
// Unlike its try/catch-wrapped siblings this reads `window.location.origin`
|
|
115
|
+
// directly, so guard it: an RN polyfill can expose `localStorage` without a
|
|
116
|
+
// `location`, which would throw here. Fail safe toward normal restore.
|
|
117
|
+
if (!hasLocalStorage() || typeof window.location === 'undefined') return false;
|
|
118
|
+
return silentRestoreSuppressed(window.localStorage, window.location.origin);
|
|
159
119
|
}
|
|
160
120
|
|
|
161
121
|
/**
|
|
@@ -180,5 +140,3 @@ export function clearSsoBounceState(): void {
|
|
|
180
140
|
// Best-effort; swallow SecurityError (e.g. Safari private mode).
|
|
181
141
|
}
|
|
182
142
|
}
|
|
183
|
-
|
|
184
|
-
export { ACTIVE_AUTHUSER_KEY };
|
|
@@ -26,19 +26,6 @@ export interface FetchSessionsWithFallbackOptions {
|
|
|
26
26
|
logger?: (message: string, error?: unknown) => void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export interface ValidateSessionBatchOptions {
|
|
30
|
-
useHeaderValidation?: boolean;
|
|
31
|
-
maxConcurrency?: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface SessionValidationResult {
|
|
35
|
-
sessionId: string;
|
|
36
|
-
valid: boolean;
|
|
37
|
-
user?: unknown;
|
|
38
|
-
raw?: unknown;
|
|
39
|
-
error?: unknown;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
29
|
/**
|
|
43
30
|
* Normalize backend session payloads into `ClientSession` objects.
|
|
44
31
|
*
|
|
@@ -99,56 +86,3 @@ export const fetchSessionsWithFallback = async (
|
|
|
99
86
|
}
|
|
100
87
|
};
|
|
101
88
|
|
|
102
|
-
/**
|
|
103
|
-
* Validate multiple sessions concurrently with configurable concurrency.
|
|
104
|
-
*
|
|
105
|
-
* @param oxyServices - Oxy service instance
|
|
106
|
-
* @param sessionIds - Session identifiers to validate
|
|
107
|
-
* @param options - Validation options
|
|
108
|
-
*/
|
|
109
|
-
export const validateSessionBatch = async (
|
|
110
|
-
oxyServices: OxyServicesAny,
|
|
111
|
-
sessionIds: string[],
|
|
112
|
-
{ useHeaderValidation = true, maxConcurrency = 5 }: ValidateSessionBatchOptions = {},
|
|
113
|
-
): Promise<SessionValidationResult[]> => {
|
|
114
|
-
if (!sessionIds.length) {
|
|
115
|
-
return [];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const uniqueSessionIds = Array.from(new Set(sessionIds));
|
|
119
|
-
const safeConcurrency = Math.max(1, Math.min(maxConcurrency, uniqueSessionIds.length));
|
|
120
|
-
const results: SessionValidationResult[] = [];
|
|
121
|
-
let index = 0;
|
|
122
|
-
|
|
123
|
-
const worker = async () => {
|
|
124
|
-
while (index < uniqueSessionIds.length) {
|
|
125
|
-
const currentIndex = index;
|
|
126
|
-
index += 1;
|
|
127
|
-
const sessionId = uniqueSessionIds[currentIndex];
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation });
|
|
131
|
-
const valid = Boolean(validation?.valid);
|
|
132
|
-
|
|
133
|
-
results.push({
|
|
134
|
-
sessionId,
|
|
135
|
-
valid,
|
|
136
|
-
user: validation?.user,
|
|
137
|
-
raw: validation,
|
|
138
|
-
});
|
|
139
|
-
} catch (error) {
|
|
140
|
-
results.push({
|
|
141
|
-
sessionId,
|
|
142
|
-
valid: false,
|
|
143
|
-
error,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
await Promise.all(Array.from({ length: safeConcurrency }, worker));
|
|
150
|
-
|
|
151
|
-
return results;
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
|
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @jest-environment node
|
|
3
3
|
*
|
|
4
|
-
* Regression coverage for the
|
|
4
|
+
* Regression coverage for the device-flow sign-in bugs:
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* a device-flow AuthSession and opens auth.oxy.so/authorize. The
|
|
8
|
-
* in successfully, the API authorizes the session and notifies
|
|
9
|
-
* the auth-session socket — but the screen then called
|
|
10
|
-
* DIRECTLY without first claiming the bearer with the
|
|
11
|
-
* The session was authorized server-side but the app
|
|
12
|
-
* authenticated ("nothing happens").
|
|
6
|
+
* BUG 1 (native): tapping "Sign In with Oxy" opens `OxyAuthScreen`, which
|
|
7
|
+
* creates a device-flow AuthSession and opens auth.oxy.so/authorize. The
|
|
8
|
+
* user signs in successfully, the API authorizes the session and notifies
|
|
9
|
+
* the client via the auth-session socket — but the screen then called
|
|
10
|
+
* `switchSession` DIRECTLY without first claiming the bearer with the
|
|
11
|
+
* secret `sessionToken`. The session was authorized server-side but the app
|
|
12
|
+
* never became authenticated ("nothing happens").
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* `completeDeviceFlowSignIn`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* BUG 2 (session-sync cutover regression): once claiming was fixed,
|
|
15
|
+
* `completeDeviceFlowSignIn` still committed the claimed session through
|
|
16
|
+
* `switchSession`. After the session-sync cutover, `switchSession` became an
|
|
17
|
+
* account-SWITCH between accounts already registered on the device and
|
|
18
|
+
* throws `No device account found for session "..."` for a freshly-claimed
|
|
19
|
+
* session that was never registered — surfacing as "Authorization
|
|
20
|
+
* successful but failed to complete sign in." The fix commits the claimed
|
|
21
|
+
* session through `commitSession` (`useOxy().handleWebSession`) instead —
|
|
22
|
+
* the same path a fresh password sign-in uses to register the account
|
|
23
|
+
* into the device's session set.
|
|
24
|
+
*
|
|
25
|
+
* These tests pin the order (claim before commit), the fail-fast behaviour
|
|
26
|
+
* on a claim that returns no usable session, and propagation of a
|
|
27
|
+
* `commitSession` failure.
|
|
18
28
|
*/
|
|
19
29
|
|
|
20
|
-
import type { User } from '@oxyhq/core';
|
|
30
|
+
import type { SessionLoginResponse, User } from '@oxyhq/core';
|
|
31
|
+
import { ssoStateKey, ssoGuardKey, ssoDestKey } from '@oxyhq/core';
|
|
21
32
|
import {
|
|
22
33
|
completeDeviceFlowSignIn,
|
|
23
34
|
type DeviceFlowClient,
|
|
@@ -25,67 +36,159 @@ import {
|
|
|
25
36
|
|
|
26
37
|
const SESSION_ID = 'session-id-123';
|
|
27
38
|
const SESSION_TOKEN = 'a'.repeat(32);
|
|
28
|
-
const
|
|
39
|
+
const ACCESS_TOKEN = 'access-token-abc';
|
|
40
|
+
const USER = {
|
|
41
|
+
id: 'user-1',
|
|
42
|
+
username: 'nate',
|
|
43
|
+
name: { displayName: 'Nate' },
|
|
44
|
+
privacySettings: {},
|
|
45
|
+
} as User;
|
|
46
|
+
// The session carries only the minimal shape (`id`/`username`/`name`/`avatar`)
|
|
47
|
+
// — this is what `commitSession` receives, not the full claimed `User`.
|
|
48
|
+
const MINIMAL_USER = { id: USER.id, username: USER.username, name: USER.name, avatar: undefined };
|
|
29
49
|
|
|
30
50
|
describe('completeDeviceFlowSignIn', () => {
|
|
31
|
-
it('claims the sessionToken BEFORE
|
|
51
|
+
it('claims the sessionToken BEFORE committing the session', async () => {
|
|
32
52
|
const order: string[] = [];
|
|
33
53
|
|
|
34
54
|
const oxyServices: DeviceFlowClient = {
|
|
55
|
+
requestSsoEstablishUrl: jest.fn(async () => ({ establishUrl: 'https://auth.oxy.so/sso/establish' })),
|
|
35
56
|
claimSessionByToken: jest.fn(async (token: string) => {
|
|
36
57
|
expect(token).toBe(SESSION_TOKEN);
|
|
37
58
|
order.push('claim');
|
|
59
|
+
return {
|
|
60
|
+
accessToken: ACCESS_TOKEN,
|
|
61
|
+
sessionId: SESSION_ID,
|
|
62
|
+
deviceId: 'device-1',
|
|
63
|
+
expiresAt: '2026-01-01T00:00:00.000Z',
|
|
64
|
+
user: USER,
|
|
65
|
+
};
|
|
38
66
|
}),
|
|
39
67
|
};
|
|
40
|
-
const
|
|
41
|
-
expect(
|
|
42
|
-
|
|
43
|
-
|
|
68
|
+
const commitSession = jest.fn(async (session: SessionLoginResponse): Promise<void> => {
|
|
69
|
+
expect(session).toEqual({
|
|
70
|
+
sessionId: SESSION_ID,
|
|
71
|
+
deviceId: 'device-1',
|
|
72
|
+
expiresAt: '2026-01-01T00:00:00.000Z',
|
|
73
|
+
user: MINIMAL_USER,
|
|
74
|
+
accessToken: ACCESS_TOKEN,
|
|
75
|
+
});
|
|
76
|
+
order.push('commit');
|
|
44
77
|
});
|
|
45
78
|
|
|
46
79
|
const user = await completeDeviceFlowSignIn({
|
|
47
80
|
oxyServices,
|
|
48
81
|
sessionId: SESSION_ID,
|
|
49
82
|
sessionToken: SESSION_TOKEN,
|
|
50
|
-
|
|
83
|
+
commitSession,
|
|
51
84
|
});
|
|
52
85
|
|
|
53
|
-
expect(order).toEqual(['claim', '
|
|
86
|
+
expect(order).toEqual(['claim', 'commit']);
|
|
54
87
|
expect(oxyServices.claimSessionByToken).toHaveBeenCalledWith(SESSION_TOKEN);
|
|
55
|
-
expect(
|
|
88
|
+
expect(commitSession).toHaveBeenCalledTimes(1);
|
|
56
89
|
expect(user).toBe(USER);
|
|
57
90
|
});
|
|
58
91
|
|
|
59
|
-
it('
|
|
92
|
+
it('falls back to the delivered sessionId/deviceId/expiresAt when the claim omits them', async () => {
|
|
93
|
+
const oxyServices: DeviceFlowClient = {
|
|
94
|
+
requestSsoEstablishUrl: jest.fn(async () => ({ establishUrl: 'https://auth.oxy.so/sso/establish' })),
|
|
95
|
+
claimSessionByToken: jest.fn(async () => ({
|
|
96
|
+
accessToken: ACCESS_TOKEN,
|
|
97
|
+
user: USER,
|
|
98
|
+
})),
|
|
99
|
+
};
|
|
100
|
+
const commitSession = jest.fn(async (): Promise<void> => {});
|
|
101
|
+
|
|
102
|
+
await completeDeviceFlowSignIn({
|
|
103
|
+
oxyServices,
|
|
104
|
+
sessionId: SESSION_ID,
|
|
105
|
+
sessionToken: SESSION_TOKEN,
|
|
106
|
+
commitSession,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(commitSession).toHaveBeenCalledWith({
|
|
110
|
+
sessionId: SESSION_ID,
|
|
111
|
+
deviceId: '',
|
|
112
|
+
expiresAt: '',
|
|
113
|
+
user: MINIMAL_USER,
|
|
114
|
+
accessToken: ACCESS_TOKEN,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('does NOT commit the session when the claim fails (the original native regression)', async () => {
|
|
60
119
|
const claimError = new Error('claim failed (401)');
|
|
61
120
|
const oxyServices: DeviceFlowClient = {
|
|
121
|
+
requestSsoEstablishUrl: jest.fn(async () => ({ establishUrl: 'https://auth.oxy.so/sso/establish' })),
|
|
62
122
|
claimSessionByToken: jest.fn(async () => {
|
|
63
123
|
throw claimError;
|
|
64
124
|
}),
|
|
65
125
|
};
|
|
66
|
-
const
|
|
126
|
+
const commitSession = jest.fn(async (): Promise<void> => {});
|
|
67
127
|
|
|
68
128
|
await expect(
|
|
69
129
|
completeDeviceFlowSignIn({
|
|
70
130
|
oxyServices,
|
|
71
131
|
sessionId: SESSION_ID,
|
|
72
132
|
sessionToken: SESSION_TOKEN,
|
|
73
|
-
|
|
133
|
+
commitSession,
|
|
74
134
|
}),
|
|
75
135
|
).rejects.toThrow('claim failed (401)');
|
|
76
136
|
|
|
77
137
|
// The bearer was never planted, so we must not attempt the bearer-protected
|
|
78
|
-
//
|
|
79
|
-
expect(
|
|
138
|
+
// commit — surfacing the failure to the caller instead.
|
|
139
|
+
expect(commitSession).not.toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('throws when the claim returns no accessToken (the session-sync regression guard)', async () => {
|
|
143
|
+
const oxyServices: DeviceFlowClient = {
|
|
144
|
+
requestSsoEstablishUrl: jest.fn(async () => ({ establishUrl: 'https://auth.oxy.so/sso/establish' })),
|
|
145
|
+
claimSessionByToken: jest.fn(async () => ({ sessionId: SESSION_ID, user: USER })),
|
|
146
|
+
};
|
|
147
|
+
const commitSession = jest.fn(async (): Promise<void> => {});
|
|
148
|
+
|
|
149
|
+
await expect(
|
|
150
|
+
completeDeviceFlowSignIn({
|
|
151
|
+
oxyServices,
|
|
152
|
+
sessionId: SESSION_ID,
|
|
153
|
+
sessionToken: SESSION_TOKEN,
|
|
154
|
+
commitSession,
|
|
155
|
+
}),
|
|
156
|
+
).rejects.toThrow('Device-flow claim did not return a usable session');
|
|
157
|
+
|
|
158
|
+
expect(commitSession).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('throws when the claim returns no user', async () => {
|
|
162
|
+
const oxyServices: DeviceFlowClient = {
|
|
163
|
+
requestSsoEstablishUrl: jest.fn(async () => ({ establishUrl: 'https://auth.oxy.so/sso/establish' })),
|
|
164
|
+
claimSessionByToken: jest.fn(async () => ({ accessToken: ACCESS_TOKEN, sessionId: SESSION_ID })),
|
|
165
|
+
};
|
|
166
|
+
const commitSession = jest.fn(async (): Promise<void> => {});
|
|
167
|
+
|
|
168
|
+
await expect(
|
|
169
|
+
completeDeviceFlowSignIn({
|
|
170
|
+
oxyServices,
|
|
171
|
+
sessionId: SESSION_ID,
|
|
172
|
+
sessionToken: SESSION_TOKEN,
|
|
173
|
+
commitSession,
|
|
174
|
+
}),
|
|
175
|
+
).rejects.toThrow('Device-flow claim did not return a usable session');
|
|
176
|
+
|
|
177
|
+
expect(commitSession).not.toHaveBeenCalled();
|
|
80
178
|
});
|
|
81
179
|
|
|
82
|
-
it('propagates a
|
|
83
|
-
const
|
|
180
|
+
it('propagates a commitSession failure after a successful claim', async () => {
|
|
181
|
+
const commitError = new Error('session invalid');
|
|
84
182
|
const oxyServices: DeviceFlowClient = {
|
|
85
|
-
|
|
183
|
+
requestSsoEstablishUrl: jest.fn(async () => ({ establishUrl: 'https://auth.oxy.so/sso/establish' })),
|
|
184
|
+
claimSessionByToken: jest.fn(async () => ({
|
|
185
|
+
accessToken: ACCESS_TOKEN,
|
|
186
|
+
sessionId: SESSION_ID,
|
|
187
|
+
user: USER,
|
|
188
|
+
})),
|
|
86
189
|
};
|
|
87
|
-
const
|
|
88
|
-
throw
|
|
190
|
+
const commitSession = jest.fn(async (): Promise<void> => {
|
|
191
|
+
throw commitError;
|
|
89
192
|
});
|
|
90
193
|
|
|
91
194
|
await expect(
|
|
@@ -93,11 +196,117 @@ describe('completeDeviceFlowSignIn', () => {
|
|
|
93
196
|
oxyServices,
|
|
94
197
|
sessionId: SESSION_ID,
|
|
95
198
|
sessionToken: SESSION_TOKEN,
|
|
96
|
-
|
|
199
|
+
commitSession,
|
|
97
200
|
}),
|
|
98
201
|
).rejects.toThrow('session invalid');
|
|
99
202
|
|
|
100
203
|
expect(oxyServices.claimSessionByToken).toHaveBeenCalledTimes(1);
|
|
101
|
-
expect(
|
|
204
|
+
expect(commitSession).toHaveBeenCalledTimes(1);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('web durable-session establish hop', () => {
|
|
208
|
+
const RP_ORIGIN = 'https://accounts.oxy.so';
|
|
209
|
+
const RP_HREF = 'https://accounts.oxy.so/settings';
|
|
210
|
+
const ESTABLISH_URL =
|
|
211
|
+
'https://auth.oxy.so/sso/establish?et=jwt&return_to=https%3A%2F%2Faccounts.oxy.so%2F__oxy%2Fsso-callback&state=state-1';
|
|
212
|
+
|
|
213
|
+
function webDeps(overrides: Record<string, unknown> = {}) {
|
|
214
|
+
const map = new Map<string, string>();
|
|
215
|
+
const navigated: string[] = [];
|
|
216
|
+
const deps = {
|
|
217
|
+
isWeb: () => true,
|
|
218
|
+
storage: {
|
|
219
|
+
getItem: (k: string) => (map.has(k) ? (map.get(k) as string) : null),
|
|
220
|
+
setItem: (k: string, v: string) => { map.set(k, v); },
|
|
221
|
+
},
|
|
222
|
+
location: { origin: RP_ORIGIN, href: RP_HREF },
|
|
223
|
+
navigate: (url: string) => { navigated.push(url); },
|
|
224
|
+
generateState: () => 'state-1',
|
|
225
|
+
now: () => 1_700_000_000_000,
|
|
226
|
+
...overrides,
|
|
227
|
+
};
|
|
228
|
+
return { deps, map, navigated };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function baseClient(
|
|
232
|
+
requestSsoEstablishUrl: DeviceFlowClient['requestSsoEstablishUrl'],
|
|
233
|
+
): DeviceFlowClient {
|
|
234
|
+
return {
|
|
235
|
+
requestSsoEstablishUrl,
|
|
236
|
+
claimSessionByToken: jest.fn(async () => ({
|
|
237
|
+
accessToken: ACCESS_TOKEN,
|
|
238
|
+
sessionId: SESSION_ID,
|
|
239
|
+
deviceId: 'device-1',
|
|
240
|
+
expiresAt: '2026-01-01T00:00:00.000Z',
|
|
241
|
+
user: USER,
|
|
242
|
+
})),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
it('fires the establish hop on web: requests the URL with the RP origin + state and navigates once', async () => {
|
|
247
|
+
const requestSsoEstablishUrl = jest.fn(async (origin: string, state: string) => {
|
|
248
|
+
expect(origin).toBe(RP_ORIGIN);
|
|
249
|
+
expect(state).toBe('state-1');
|
|
250
|
+
return { establishUrl: ESTABLISH_URL };
|
|
251
|
+
});
|
|
252
|
+
const oxyServices = baseClient(requestSsoEstablishUrl);
|
|
253
|
+
const { deps, map, navigated } = webDeps();
|
|
254
|
+
|
|
255
|
+
const user = await completeDeviceFlowSignIn({
|
|
256
|
+
oxyServices,
|
|
257
|
+
sessionId: SESSION_ID,
|
|
258
|
+
sessionToken: SESSION_TOKEN,
|
|
259
|
+
commitSession: jest.fn(async () => {}),
|
|
260
|
+
establishDeps: deps,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
expect(user).toBe(USER);
|
|
264
|
+
expect(requestSsoEstablishUrl).toHaveBeenCalledTimes(1);
|
|
265
|
+
expect(navigated).toEqual([ESTABLISH_URL]);
|
|
266
|
+
// Bounce state persisted so the post-bounce `sso-return` step validates it.
|
|
267
|
+
expect(map.get(ssoStateKey(RP_ORIGIN))).toBe('state-1');
|
|
268
|
+
expect(map.get(ssoGuardKey(RP_ORIGIN))).toBe(String(1_700_000_000_000));
|
|
269
|
+
expect(map.get(ssoDestKey(RP_ORIGIN))).toBe(RP_HREF);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('does NOT fire the hop on native (no request, no navigation)', async () => {
|
|
273
|
+
const requestSsoEstablishUrl = jest.fn(async () => ({ establishUrl: ESTABLISH_URL }));
|
|
274
|
+
const oxyServices = baseClient(requestSsoEstablishUrl);
|
|
275
|
+
const { deps, navigated } = webDeps({ isWeb: () => false });
|
|
276
|
+
|
|
277
|
+
await completeDeviceFlowSignIn({
|
|
278
|
+
oxyServices,
|
|
279
|
+
sessionId: SESSION_ID,
|
|
280
|
+
sessionToken: SESSION_TOKEN,
|
|
281
|
+
commitSession: jest.fn(async () => {}),
|
|
282
|
+
establishDeps: deps,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(requestSsoEstablishUrl).not.toHaveBeenCalled();
|
|
286
|
+
expect(navigated).toEqual([]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('completes sign-in even if the establish request fails — exactly one attempt, no navigation', async () => {
|
|
290
|
+
const requestSsoEstablishUrl = jest.fn(async () => {
|
|
291
|
+
throw new Error('403 unapproved');
|
|
292
|
+
});
|
|
293
|
+
const oxyServices = baseClient(requestSsoEstablishUrl);
|
|
294
|
+
const { deps, map, navigated } = webDeps();
|
|
295
|
+
|
|
296
|
+
const user = await completeDeviceFlowSignIn({
|
|
297
|
+
oxyServices,
|
|
298
|
+
sessionId: SESSION_ID,
|
|
299
|
+
sessionToken: SESSION_TOKEN,
|
|
300
|
+
commitSession: jest.fn(async () => {}),
|
|
301
|
+
establishDeps: deps,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Sign-in still completed (the primitive is total — never throws).
|
|
305
|
+
expect(user).toBe(USER);
|
|
306
|
+
expect(requestSsoEstablishUrl).toHaveBeenCalledTimes(1);
|
|
307
|
+
expect(navigated).toEqual([]);
|
|
308
|
+
// No stale bounce state left behind on failure.
|
|
309
|
+
expect(map.size).toBe(0);
|
|
310
|
+
});
|
|
102
311
|
});
|
|
103
312
|
});
|