@oxyhq/core 5.5.0 → 7.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 (196) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +7 -4
  3. package/dist/cjs/OxyServices.base.js +7 -102
  4. package/dist/cjs/boot/coldBootV2.js +350 -0
  5. package/dist/cjs/boot/deviceBootReturn.js +152 -0
  6. package/dist/cjs/crypto/keyManager.js +95 -0
  7. package/dist/cjs/i18n/locales/en-US.json +13 -1
  8. package/dist/cjs/i18n/locales/es-ES.json +13 -1
  9. package/dist/cjs/i18n/locales/locales/en-US.json +13 -1
  10. package/dist/cjs/i18n/locales/locales/es-ES.json +13 -1
  11. package/dist/cjs/index.js +54 -45
  12. package/dist/cjs/mixins/OxyServices.accounts.js +13 -26
  13. package/dist/cjs/mixins/OxyServices.auth.js +66 -201
  14. package/dist/cjs/mixins/OxyServices.deviceBoot.js +119 -0
  15. package/dist/cjs/mixins/index.js +8 -17
  16. package/dist/cjs/server/index.js +2 -2
  17. package/dist/cjs/session/SessionClient.js +40 -1
  18. package/dist/cjs/session/authStateStore.js +284 -0
  19. package/dist/cjs/session/refresh.js +264 -0
  20. package/dist/cjs/shared/utils/debugUtils.js +3 -3
  21. package/dist/cjs/utils/accountUtils.js +1 -55
  22. package/dist/cjs/utils/authWebUrl.js +10 -39
  23. package/dist/cjs/utils/coldBoot.js +10 -8
  24. package/dist/cjs/utils/platform.js +19 -0
  25. package/dist/cjs/utils/registrableApex.js +49 -0
  26. package/dist/cjs/utils/ssoBounce.js +15 -362
  27. package/dist/cjs/utils/validationUtils.js +57 -0
  28. package/dist/esm/.tsbuildinfo +1 -1
  29. package/dist/esm/HttpService.js +7 -4
  30. package/dist/esm/OxyServices.base.js +7 -102
  31. package/dist/esm/boot/coldBootV2.js +344 -0
  32. package/dist/esm/boot/deviceBootReturn.js +146 -0
  33. package/dist/esm/crypto/keyManager.js +95 -0
  34. package/dist/esm/i18n/locales/en-US.json +13 -1
  35. package/dist/esm/i18n/locales/es-ES.json +13 -1
  36. package/dist/esm/i18n/locales/locales/en-US.json +13 -1
  37. package/dist/esm/i18n/locales/locales/es-ES.json +13 -1
  38. package/dist/esm/index.js +34 -17
  39. package/dist/esm/mixins/OxyServices.accounts.js +13 -26
  40. package/dist/esm/mixins/OxyServices.auth.js +66 -201
  41. package/dist/esm/mixins/OxyServices.deviceBoot.js +116 -0
  42. package/dist/esm/mixins/index.js +8 -17
  43. package/dist/esm/server/index.js +1 -1
  44. package/dist/esm/session/SessionClient.js +40 -1
  45. package/dist/esm/session/authStateStore.js +278 -0
  46. package/dist/esm/session/refresh.js +257 -0
  47. package/dist/esm/shared/utils/debugUtils.js +3 -3
  48. package/dist/esm/utils/accountUtils.js +0 -53
  49. package/dist/esm/utils/authWebUrl.js +9 -37
  50. package/dist/esm/utils/coldBoot.js +10 -8
  51. package/dist/esm/utils/platform.js +18 -0
  52. package/dist/esm/utils/registrableApex.js +46 -0
  53. package/dist/esm/utils/ssoBounce.js +14 -345
  54. package/dist/esm/utils/validationUtils.js +56 -0
  55. package/dist/types/.tsbuildinfo +1 -1
  56. package/dist/types/HttpService.d.ts +14 -1
  57. package/dist/types/OxyServices.base.d.ts +0 -52
  58. package/dist/types/OxyServices.d.ts +0 -25
  59. package/dist/types/boot/coldBootV2.d.ts +76 -0
  60. package/dist/types/boot/deviceBootReturn.d.ts +83 -0
  61. package/dist/types/crypto/keyManager.d.ts +21 -0
  62. package/dist/types/index.d.ts +14 -21
  63. package/dist/types/mixins/OxyServices.accounts.d.ts +24 -29
  64. package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
  65. package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
  66. package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
  67. package/dist/types/mixins/OxyServices.auth.d.ts +35 -77
  68. package/dist/types/mixins/OxyServices.civic.d.ts +0 -2
  69. package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
  70. package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
  71. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +110 -0
  72. package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
  73. package/dist/types/mixins/OxyServices.features.d.ts +0 -2
  74. package/dist/types/mixins/OxyServices.identity.d.ts +0 -2
  75. package/dist/types/mixins/OxyServices.language.d.ts +0 -2
  76. package/dist/types/mixins/OxyServices.links.d.ts +0 -2
  77. package/dist/types/mixins/OxyServices.location.d.ts +0 -2
  78. package/dist/types/mixins/OxyServices.nodes.d.ts +0 -2
  79. package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
  80. package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
  81. package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
  82. package/dist/types/mixins/OxyServices.security.d.ts +0 -2
  83. package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
  84. package/dist/types/mixins/OxyServices.user.d.ts +0 -2
  85. package/dist/types/mixins/OxyServices.utility.d.ts +0 -2
  86. package/dist/types/mixins/index.d.ts +5 -9
  87. package/dist/types/models/interfaces.d.ts +0 -67
  88. package/dist/types/models/session.d.ts +4 -5
  89. package/dist/types/server/index.d.ts +1 -1
  90. package/dist/types/session/SessionClient.d.ts +26 -1
  91. package/dist/types/session/authStateStore.d.ts +119 -0
  92. package/dist/types/session/refresh.d.ts +93 -0
  93. package/dist/types/shared/utils/debugUtils.d.ts +3 -3
  94. package/dist/types/utils/accountUtils.d.ts +2 -17
  95. package/dist/types/utils/authWebUrl.d.ts +9 -35
  96. package/dist/types/utils/coldBoot.d.ts +17 -14
  97. package/dist/types/utils/platform.d.ts +14 -0
  98. package/dist/types/utils/registrableApex.d.ts +31 -0
  99. package/dist/types/utils/ssoBounce.d.ts +14 -280
  100. package/dist/types/utils/validationUtils.d.ts +15 -0
  101. package/package.json +2 -2
  102. package/src/HttpService.ts +20 -4
  103. package/src/OxyServices.base.ts +7 -112
  104. package/src/OxyServices.ts +0 -38
  105. package/src/boot/__tests__/coldBootV2.test.ts +317 -0
  106. package/src/boot/__tests__/deviceBootReturn.test.ts +158 -0
  107. package/src/boot/coldBootV2.ts +426 -0
  108. package/src/boot/deviceBootReturn.ts +195 -0
  109. package/src/crypto/__tests__/sharedDeviceToken.test.ts +24 -0
  110. package/src/crypto/keyManager.ts +101 -0
  111. package/src/i18n/locales/en-US.json +13 -1
  112. package/src/i18n/locales/es-ES.json +13 -1
  113. package/src/index.ts +75 -65
  114. package/src/mixins/OxyServices.accounts.ts +27 -39
  115. package/src/mixins/OxyServices.auth.ts +78 -253
  116. package/src/mixins/OxyServices.deviceBoot.ts +146 -0
  117. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +107 -0
  118. package/src/mixins/__tests__/accounts.test.ts +17 -44
  119. package/src/mixins/__tests__/passwordSignIn.test.ts +91 -0
  120. package/src/mixins/index.ts +10 -23
  121. package/src/models/interfaces.ts +0 -79
  122. package/src/models/session.ts +4 -5
  123. package/src/server/index.ts +1 -1
  124. package/src/session/SessionClient.ts +53 -2
  125. package/src/session/__tests__/SessionClient.additive.test.ts +92 -0
  126. package/src/session/__tests__/SessionClient.rest.test.ts +25 -0
  127. package/src/session/__tests__/SessionClient.state.test.ts +18 -5
  128. package/src/session/__tests__/authStateStore.test.ts +209 -0
  129. package/src/session/__tests__/refresh.test.ts +256 -0
  130. package/src/session/authStateStore.ts +335 -0
  131. package/src/session/refresh.ts +334 -0
  132. package/src/shared/utils/debugUtils.ts +3 -3
  133. package/src/utils/__tests__/authWebUrl.test.ts +5 -40
  134. package/src/utils/__tests__/registrableApex.test.ts +62 -0
  135. package/src/utils/__tests__/validationUtils.test.ts +30 -0
  136. package/src/utils/accountUtils.ts +2 -65
  137. package/src/utils/authWebUrl.ts +9 -39
  138. package/src/utils/coldBoot.ts +17 -14
  139. package/src/utils/platform.ts +21 -0
  140. package/src/utils/registrableApex.ts +45 -0
  141. package/src/utils/ssoBounce.ts +14 -393
  142. package/src/utils/validationUtils.ts +62 -0
  143. package/dist/cjs/AuthManager.js +0 -1110
  144. package/dist/cjs/AuthManagerTypes.js +0 -13
  145. package/dist/cjs/CrossDomainAuth.js +0 -206
  146. package/dist/cjs/mixins/OxyServices.fedcm.js +0 -823
  147. package/dist/cjs/mixins/OxyServices.redirect.js +0 -95
  148. package/dist/cjs/mixins/OxyServices.silent.js +0 -204
  149. package/dist/cjs/mixins/OxyServices.sso.js +0 -208
  150. package/dist/cjs/utils/fapiAutoDetect.js +0 -99
  151. package/dist/cjs/utils/ssoEstablish.js +0 -110
  152. package/dist/cjs/utils/ssoReturn.js +0 -275
  153. package/dist/esm/AuthManager.js +0 -1105
  154. package/dist/esm/AuthManagerTypes.js +0 -12
  155. package/dist/esm/CrossDomainAuth.js +0 -201
  156. package/dist/esm/mixins/OxyServices.fedcm.js +0 -821
  157. package/dist/esm/mixins/OxyServices.redirect.js +0 -92
  158. package/dist/esm/mixins/OxyServices.silent.js +0 -202
  159. package/dist/esm/mixins/OxyServices.sso.js +0 -204
  160. package/dist/esm/utils/fapiAutoDetect.js +0 -95
  161. package/dist/esm/utils/ssoEstablish.js +0 -107
  162. package/dist/esm/utils/ssoReturn.js +0 -271
  163. package/dist/types/AuthManager.d.ts +0 -380
  164. package/dist/types/AuthManagerTypes.d.ts +0 -81
  165. package/dist/types/CrossDomainAuth.d.ts +0 -164
  166. package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -331
  167. package/dist/types/mixins/OxyServices.redirect.d.ts +0 -92
  168. package/dist/types/mixins/OxyServices.silent.d.ts +0 -132
  169. package/dist/types/mixins/OxyServices.sso.d.ts +0 -138
  170. package/dist/types/utils/fapiAutoDetect.d.ts +0 -56
  171. package/dist/types/utils/ssoEstablish.d.ts +0 -85
  172. package/dist/types/utils/ssoReturn.d.ts +0 -156
  173. package/src/AuthManager.ts +0 -1269
  174. package/src/AuthManagerTypes.ts +0 -86
  175. package/src/CrossDomainAuth.ts +0 -243
  176. package/src/__tests__/authManager.cookiePath.test.ts +0 -390
  177. package/src/__tests__/authManager.security.test.ts +0 -377
  178. package/src/__tests__/crossDomainAuth.test.ts +0 -116
  179. package/src/__tests__/establishDeviceRefreshSlot.test.ts +0 -221
  180. package/src/mixins/OxyServices.fedcm.ts +0 -1026
  181. package/src/mixins/OxyServices.redirect.ts +0 -122
  182. package/src/mixins/OxyServices.silent.ts +0 -272
  183. package/src/mixins/OxyServices.sso.ts +0 -261
  184. package/src/mixins/__tests__/constructorAuthWebUrl.test.ts +0 -85
  185. package/src/mixins/__tests__/fedcm.test.ts +0 -667
  186. package/src/mixins/__tests__/sessionBaseUrl.test.ts +0 -61
  187. package/src/mixins/__tests__/silent.test.ts +0 -102
  188. package/src/mixins/__tests__/sso.test.ts +0 -228
  189. package/src/utils/__tests__/consumeSsoReturn.test.ts +0 -816
  190. package/src/utils/__tests__/fapiAutoDetect.test.ts +0 -183
  191. package/src/utils/__tests__/ssoBounce.test.ts +0 -219
  192. package/src/utils/__tests__/ssoEstablish.test.ts +0 -204
  193. package/src/utils/__tests__/ssoReturn.test.ts +0 -276
  194. package/src/utils/fapiAutoDetect.ts +0 -91
  195. package/src/utils/ssoEstablish.ts +0 -174
  196. package/src/utils/ssoReturn.ts +0 -389
@@ -1,221 +0,0 @@
1
- /**
2
- * `establishDeviceRefreshSlot` + primary-session slot-registration tests.
3
- *
4
- * ROOT CAUSE these lock in: a web PRIMARY session (FedCM exchange, central `/sso`
5
- * return, IdP `/auth/silent` iframe, password) plants only an access token. The
6
- * cross-origin/credential-less restore endpoints (`/sso/exchange`, `/auth/silent`)
7
- * cannot set the device's `oxy_rt_<authuser>` cookie, so without an explicit
8
- * `POST /auth/session` the primary never joins the device refresh-cookie set:
9
- * `/auth/refresh-all` returns zero accounts and account-switch persistence has no
10
- * foundation. `establishDeviceRefreshSlot()` is the single shared primitive (on the
11
- * base, reused by `switchToAccount` and `AuthManager.handleAuthSuccess`) that plants
12
- * the slot where the cookie is visible, re-plants the rotated token, and returns the
13
- * authoritative `authuser`.
14
- *
15
- * `testEnvironment` is `node`, where `getPlatformOS()` resolves to `'web'`
16
- * (`isWeb()` is true) and `window` is undefined — so the FIRST-PARTY apex gate is
17
- * inert and the method exercises its happy path. Browser-only apex gating is
18
- * verified separately by stubbing a `window`.
19
- */
20
-
21
- import { OxyServices } from '../OxyServices';
22
- import { AuthManager } from '../AuthManager';
23
- import type { StorageAdapter } from '../AuthManager';
24
- import type { RefreshAllResponse, RefreshCookieResponse, User } from '../models/interfaces';
25
- import type { SessionLoginResponse } from '../models/session';
26
-
27
- function buildAccessToken(claims: Record<string, unknown>): string {
28
- const b64url = (value: string): string =>
29
- Buffer.from(value).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
30
- const header = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
31
- const payload = b64url(JSON.stringify(claims));
32
- return `${header}.${payload}.signature`;
33
- }
34
-
35
- describe('OxyServices.establishDeviceRefreshSlot', () => {
36
- let oxy: OxyServices;
37
- let makeRequestSpy: jest.SpyInstance;
38
-
39
- beforeEach(() => {
40
- oxy = new OxyServices({ baseURL: 'http://test.invalid' });
41
- oxy.httpService.setTokens('primary-token');
42
- makeRequestSpy = jest.spyOn(oxy, 'makeRequest');
43
- });
44
-
45
- afterEach(() => {
46
- makeRequestSpy.mockRestore();
47
- });
48
-
49
- it('POSTs to /auth/session, re-plants the rotated access token, and returns the server authuser', async () => {
50
- makeRequestSpy.mockResolvedValue({ accessToken: 'rotated-token', authuser: 2 });
51
-
52
- const authuser = await oxy.establishDeviceRefreshSlot();
53
-
54
- expect(authuser).toBe(2);
55
- expect(makeRequestSpy).toHaveBeenCalledWith('POST', '/auth/session', undefined, { cache: false });
56
- // The rotated token from /auth/session is now the active token.
57
- expect(oxy.getAccessToken()).toBe('rotated-token');
58
- });
59
-
60
- it('returns the authuser even when the response carries no rotated token (keeps the existing token)', async () => {
61
- makeRequestSpy.mockResolvedValue({ authuser: 0 });
62
-
63
- const authuser = await oxy.establishDeviceRefreshSlot();
64
-
65
- expect(authuser).toBe(0);
66
- expect(oxy.getAccessToken()).toBe('primary-token');
67
- });
68
-
69
- it('returns null when the server omits a numeric authuser', async () => {
70
- makeRequestSpy.mockResolvedValue({ accessToken: 'rotated-token' });
71
-
72
- const authuser = await oxy.establishDeviceRefreshSlot();
73
-
74
- expect(authuser).toBeNull();
75
- });
76
-
77
- it('is best-effort: a failed /auth/session resolves null and never throws', async () => {
78
- makeRequestSpy.mockRejectedValue(new Error('network down'));
79
-
80
- await expect(oxy.establishDeviceRefreshSlot()).resolves.toBeNull();
81
- });
82
-
83
- it('FIRST-PARTY gate: skips /auth/session when the page apex differs from the API apex (cross-apex RP)', async () => {
84
- // Simulate a cross-apex RP page (mention.earth) calling api.oxy.so.
85
- const rp = new OxyServices({ baseURL: 'https://api.oxy.so' });
86
- rp.httpService.setTokens('primary-token');
87
- const rpSpy = jest.spyOn(rp, 'makeRequest');
88
- const originalWindow = (globalThis as { window?: unknown }).window;
89
- (globalThis as { window?: unknown }).window = { location: { hostname: 'mention.earth' } };
90
- try {
91
- const authuser = await rp.establishDeviceRefreshSlot();
92
- expect(authuser).toBeNull();
93
- expect(rpSpy).not.toHaveBeenCalled();
94
- } finally {
95
- if (originalWindow === undefined) {
96
- delete (globalThis as { window?: unknown }).window;
97
- } else {
98
- (globalThis as { window?: unknown }).window = originalWindow;
99
- }
100
- rpSpy.mockRestore();
101
- }
102
- });
103
-
104
- it('FIRST-PARTY gate: proceeds when the page apex matches the API apex (first-party *.oxy.so)', async () => {
105
- const fp = new OxyServices({ baseURL: 'https://api.oxy.so' });
106
- fp.httpService.setTokens('primary-token');
107
- const fpSpy = jest.spyOn(fp, 'makeRequest').mockResolvedValue({ accessToken: 'rotated', authuser: 1 });
108
- const originalWindow = (globalThis as { window?: unknown }).window;
109
- (globalThis as { window?: unknown }).window = { location: { hostname: 'accounts.oxy.so' } };
110
- try {
111
- const authuser = await fp.establishDeviceRefreshSlot();
112
- expect(authuser).toBe(1);
113
- expect(fpSpy).toHaveBeenCalledWith('POST', '/auth/session', undefined, { cache: false });
114
- } finally {
115
- if (originalWindow === undefined) {
116
- delete (globalThis as { window?: unknown }).window;
117
- } else {
118
- (globalThis as { window?: unknown }).window = originalWindow;
119
- }
120
- fpSpy.mockRestore();
121
- }
122
- });
123
- });
124
-
125
- // ---------------------------------------------------------------------------
126
- // AuthManager.handleAuthSuccess — every web primary now joins the device set
127
- // ---------------------------------------------------------------------------
128
-
129
- class InMemoryStorage implements StorageAdapter {
130
- private store = new Map<string, string>();
131
- getItem(key: string): string | null { return this.store.get(key) ?? null; }
132
- setItem(key: string, value: string): void { this.store.set(key, value); }
133
- removeItem(key: string): void { this.store.delete(key); }
134
- raw(): Map<string, string> { return this.store; }
135
- }
136
-
137
- interface MockHttpService {
138
- setTokens: jest.Mock;
139
- setAuthRefreshHandler: jest.Mock;
140
- }
141
-
142
- interface MockServices {
143
- establishDeviceRefreshSlot: jest.Mock<Promise<number | null>, []>;
144
- getAccessToken: jest.Mock<string | null, []>;
145
- refreshAllSessions: jest.Mock<Promise<RefreshAllResponse>, []>;
146
- refreshTokenViaCookie: jest.Mock<Promise<RefreshCookieResponse | null>, [{ authuser?: number }]>;
147
- logoutSessionByAuthuser: jest.Mock<Promise<void>, [number]>;
148
- logoutAllSessionsViaCookie: jest.Mock<Promise<void>, []>;
149
- getCurrentUser: jest.Mock<Promise<User>, []>;
150
- httpService: MockHttpService;
151
- }
152
-
153
- const ACTIVE_AUTHUSER_KEY = 'oxy_active_authuser';
154
-
155
- function makeMockServices(): MockServices {
156
- return {
157
- establishDeviceRefreshSlot: jest.fn(async () => null),
158
- getAccessToken: jest.fn(() => null),
159
- refreshAllSessions: jest.fn(async (): Promise<RefreshAllResponse> => ({ accounts: [] })),
160
- refreshTokenViaCookie: jest.fn(),
161
- logoutSessionByAuthuser: jest.fn(async () => undefined),
162
- logoutAllSessionsViaCookie: jest.fn(async () => undefined),
163
- getCurrentUser: jest.fn(async (): Promise<User> => ({ id: 'user-fedcm', publicKey: '0xabc', username: 'nate' } as User)),
164
- httpService: { setTokens: jest.fn(), setAuthRefreshHandler: jest.fn() },
165
- };
166
- }
167
-
168
- function makeManager(services: MockServices, storage: InMemoryStorage): AuthManager {
169
- return new AuthManager(services as unknown as OxyServices, {
170
- storage,
171
- autoRefresh: false,
172
- crossTabSync: false,
173
- });
174
- }
175
-
176
- function fedcmSession(): SessionLoginResponse {
177
- // No `authuser` claim in the FedCM-restored token — mirrors a cross-domain
178
- // restore whose token is not slot-bound until /auth/session runs.
179
- return {
180
- accessToken: buildAccessToken({ sessionId: 'sess-fedcm', userId: 'user-fedcm', exp: 9999999999 }),
181
- sessionId: 'sess-fedcm',
182
- deviceId: 'dev-1',
183
- expiresAt: '2099-01-01T00:00:00.000Z',
184
- user: { id: 'user-fedcm', username: 'nate', name: { displayName: 'Nate' } },
185
- } as SessionLoginResponse;
186
- }
187
-
188
- describe('AuthManager.handleAuthSuccess — primary session joins the device set', () => {
189
- it('establishes the device refresh slot and adopts the server-authoritative authuser', async () => {
190
- const services = makeMockServices();
191
- // /auth/session allocated slot 3 and rotated the token.
192
- services.establishDeviceRefreshSlot.mockResolvedValueOnce(3);
193
- const rotated = buildAccessToken({ sessionId: 'sess-fedcm', userId: 'user-fedcm', authuser: 3, exp: 9999999999 });
194
- services.getAccessToken.mockReturnValue(rotated);
195
- const storage = new InMemoryStorage();
196
- const manager = makeManager(services, storage);
197
-
198
- await manager.handleAuthSuccess(fedcmSession(), 'fedcm');
199
-
200
- expect(services.establishDeviceRefreshSlot).toHaveBeenCalledTimes(1);
201
- // The authoritative slot from /auth/session is recorded as the active account.
202
- expect(manager.getActiveAuthuser()).toBe(3);
203
- expect(storage.raw().get(ACTIVE_AUTHUSER_KEY)).toBe('3');
204
- expect(manager.getActiveAccount()?.authuser).toBe(3);
205
- });
206
-
207
- it('falls back to the JWT-decoded authuser (then 0) when /auth/session is a no-op (native / cross-apex)', async () => {
208
- const services = makeMockServices();
209
- services.establishDeviceRefreshSlot.mockResolvedValueOnce(null);
210
- const storage = new InMemoryStorage();
211
- const manager = makeManager(services, storage);
212
-
213
- // FedCM session token carries no authuser claim → falls back to slot 0.
214
- await manager.handleAuthSuccess(fedcmSession(), 'fedcm');
215
-
216
- expect(manager.getActiveAuthuser()).toBe(0);
217
- // getAccessToken() is only consulted for the rotated token when a slot was
218
- // actually established — a null slot must not re-read it.
219
- expect(services.getAccessToken).not.toHaveBeenCalled();
220
- });
221
- });