@oxyhq/core 5.5.0 → 6.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 (181) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +6 -3
  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 +47 -44
  12. package/dist/cjs/mixins/OxyServices.accounts.js +6 -13
  13. package/dist/cjs/mixins/OxyServices.auth.js +66 -201
  14. package/dist/cjs/mixins/OxyServices.authorizedApps.js +38 -0
  15. package/dist/cjs/mixins/OxyServices.deviceBoot.js +119 -0
  16. package/dist/cjs/mixins/index.js +13 -17
  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/utils/accountUtils.js +1 -55
  21. package/dist/cjs/utils/authWebUrl.js +6 -15
  22. package/dist/cjs/utils/fapiAutoDetect.js +16 -64
  23. package/dist/cjs/utils/platform.js +19 -0
  24. package/dist/cjs/utils/ssoBounce.js +15 -362
  25. package/dist/cjs/utils/validationUtils.js +57 -0
  26. package/dist/esm/.tsbuildinfo +1 -1
  27. package/dist/esm/HttpService.js +6 -3
  28. package/dist/esm/OxyServices.base.js +7 -102
  29. package/dist/esm/boot/coldBootV2.js +344 -0
  30. package/dist/esm/boot/deviceBootReturn.js +146 -0
  31. package/dist/esm/crypto/keyManager.js +95 -0
  32. package/dist/esm/i18n/locales/en-US.json +13 -1
  33. package/dist/esm/i18n/locales/es-ES.json +13 -1
  34. package/dist/esm/i18n/locales/locales/en-US.json +13 -1
  35. package/dist/esm/i18n/locales/locales/es-ES.json +13 -1
  36. package/dist/esm/index.js +28 -18
  37. package/dist/esm/mixins/OxyServices.accounts.js +6 -13
  38. package/dist/esm/mixins/OxyServices.auth.js +66 -201
  39. package/dist/esm/mixins/OxyServices.authorizedApps.js +35 -0
  40. package/dist/esm/mixins/OxyServices.deviceBoot.js +116 -0
  41. package/dist/esm/mixins/index.js +13 -17
  42. package/dist/esm/session/SessionClient.js +40 -1
  43. package/dist/esm/session/authStateStore.js +278 -0
  44. package/dist/esm/session/refresh.js +257 -0
  45. package/dist/esm/utils/accountUtils.js +0 -53
  46. package/dist/esm/utils/authWebUrl.js +6 -14
  47. package/dist/esm/utils/fapiAutoDetect.js +16 -63
  48. package/dist/esm/utils/platform.js +18 -0
  49. package/dist/esm/utils/ssoBounce.js +14 -345
  50. package/dist/esm/utils/validationUtils.js +56 -0
  51. package/dist/types/.tsbuildinfo +1 -1
  52. package/dist/types/HttpService.d.ts +13 -0
  53. package/dist/types/OxyServices.base.d.ts +0 -52
  54. package/dist/types/OxyServices.d.ts +0 -25
  55. package/dist/types/boot/coldBootV2.d.ts +76 -0
  56. package/dist/types/boot/deviceBootReturn.d.ts +83 -0
  57. package/dist/types/crypto/keyManager.d.ts +21 -0
  58. package/dist/types/index.d.ts +15 -21
  59. package/dist/types/mixins/OxyServices.accounts.d.ts +0 -2
  60. package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
  61. package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
  62. package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
  63. package/dist/types/mixins/OxyServices.auth.d.ts +35 -77
  64. package/dist/types/mixins/{OxyServices.redirect.d.ts → OxyServices.authorizedApps.d.ts} +35 -33
  65. package/dist/types/mixins/OxyServices.civic.d.ts +0 -2
  66. package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
  67. package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
  68. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +110 -0
  69. package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
  70. package/dist/types/mixins/OxyServices.features.d.ts +0 -2
  71. package/dist/types/mixins/OxyServices.identity.d.ts +0 -2
  72. package/dist/types/mixins/OxyServices.language.d.ts +0 -2
  73. package/dist/types/mixins/OxyServices.links.d.ts +0 -2
  74. package/dist/types/mixins/OxyServices.location.d.ts +0 -2
  75. package/dist/types/mixins/OxyServices.nodes.d.ts +0 -2
  76. package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
  77. package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
  78. package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
  79. package/dist/types/mixins/OxyServices.security.d.ts +0 -2
  80. package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
  81. package/dist/types/mixins/OxyServices.user.d.ts +0 -2
  82. package/dist/types/mixins/OxyServices.utility.d.ts +0 -2
  83. package/dist/types/mixins/index.d.ts +6 -9
  84. package/dist/types/models/interfaces.d.ts +0 -67
  85. package/dist/types/session/SessionClient.d.ts +25 -0
  86. package/dist/types/session/authStateStore.d.ts +119 -0
  87. package/dist/types/session/refresh.d.ts +93 -0
  88. package/dist/types/utils/accountUtils.d.ts +0 -14
  89. package/dist/types/utils/authWebUrl.d.ts +6 -12
  90. package/dist/types/utils/fapiAutoDetect.d.ts +15 -38
  91. package/dist/types/utils/platform.d.ts +14 -0
  92. package/dist/types/utils/ssoBounce.d.ts +14 -280
  93. package/dist/types/utils/validationUtils.d.ts +15 -0
  94. package/package.json +2 -2
  95. package/src/HttpService.ts +19 -3
  96. package/src/OxyServices.base.ts +7 -112
  97. package/src/OxyServices.ts +0 -38
  98. package/src/boot/__tests__/coldBootV2.test.ts +317 -0
  99. package/src/boot/__tests__/deviceBootReturn.test.ts +158 -0
  100. package/src/boot/coldBootV2.ts +426 -0
  101. package/src/boot/deviceBootReturn.ts +195 -0
  102. package/src/crypto/__tests__/sharedDeviceToken.test.ts +24 -0
  103. package/src/crypto/keyManager.ts +101 -0
  104. package/src/i18n/locales/en-US.json +13 -1
  105. package/src/i18n/locales/es-ES.json +13 -1
  106. package/src/index.ts +74 -65
  107. package/src/mixins/OxyServices.accounts.ts +6 -13
  108. package/src/mixins/OxyServices.auth.ts +78 -253
  109. package/src/mixins/OxyServices.authorizedApps.ts +75 -0
  110. package/src/mixins/OxyServices.deviceBoot.ts +146 -0
  111. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +107 -0
  112. package/src/mixins/__tests__/accounts.test.ts +17 -44
  113. package/src/mixins/__tests__/authorizedApps.test.ts +63 -0
  114. package/src/mixins/__tests__/passwordSignIn.test.ts +91 -0
  115. package/src/mixins/index.ts +16 -22
  116. package/src/models/interfaces.ts +0 -79
  117. package/src/session/SessionClient.ts +52 -1
  118. package/src/session/__tests__/SessionClient.additive.test.ts +92 -0
  119. package/src/session/__tests__/SessionClient.rest.test.ts +25 -0
  120. package/src/session/__tests__/SessionClient.state.test.ts +18 -5
  121. package/src/session/__tests__/authStateStore.test.ts +209 -0
  122. package/src/session/__tests__/refresh.test.ts +256 -0
  123. package/src/session/authStateStore.ts +335 -0
  124. package/src/session/refresh.ts +334 -0
  125. package/src/utils/__tests__/authWebUrl.test.ts +5 -29
  126. package/src/utils/__tests__/fapiAutoDetect.test.ts +5 -126
  127. package/src/utils/__tests__/validationUtils.test.ts +30 -0
  128. package/src/utils/accountUtils.ts +0 -62
  129. package/src/utils/authWebUrl.ts +6 -15
  130. package/src/utils/fapiAutoDetect.ts +16 -60
  131. package/src/utils/platform.ts +21 -0
  132. package/src/utils/ssoBounce.ts +14 -393
  133. package/src/utils/validationUtils.ts +62 -0
  134. package/dist/cjs/AuthManager.js +0 -1110
  135. package/dist/cjs/AuthManagerTypes.js +0 -13
  136. package/dist/cjs/CrossDomainAuth.js +0 -206
  137. package/dist/cjs/mixins/OxyServices.fedcm.js +0 -823
  138. package/dist/cjs/mixins/OxyServices.redirect.js +0 -95
  139. package/dist/cjs/mixins/OxyServices.silent.js +0 -204
  140. package/dist/cjs/mixins/OxyServices.sso.js +0 -208
  141. package/dist/cjs/utils/ssoEstablish.js +0 -110
  142. package/dist/cjs/utils/ssoReturn.js +0 -275
  143. package/dist/esm/AuthManager.js +0 -1105
  144. package/dist/esm/AuthManagerTypes.js +0 -12
  145. package/dist/esm/CrossDomainAuth.js +0 -201
  146. package/dist/esm/mixins/OxyServices.fedcm.js +0 -821
  147. package/dist/esm/mixins/OxyServices.redirect.js +0 -92
  148. package/dist/esm/mixins/OxyServices.silent.js +0 -202
  149. package/dist/esm/mixins/OxyServices.sso.js +0 -204
  150. package/dist/esm/utils/ssoEstablish.js +0 -107
  151. package/dist/esm/utils/ssoReturn.js +0 -271
  152. package/dist/types/AuthManager.d.ts +0 -380
  153. package/dist/types/AuthManagerTypes.d.ts +0 -81
  154. package/dist/types/CrossDomainAuth.d.ts +0 -164
  155. package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -331
  156. package/dist/types/mixins/OxyServices.silent.d.ts +0 -132
  157. package/dist/types/mixins/OxyServices.sso.d.ts +0 -138
  158. package/dist/types/utils/ssoEstablish.d.ts +0 -85
  159. package/dist/types/utils/ssoReturn.d.ts +0 -156
  160. package/src/AuthManager.ts +0 -1269
  161. package/src/AuthManagerTypes.ts +0 -86
  162. package/src/CrossDomainAuth.ts +0 -243
  163. package/src/__tests__/authManager.cookiePath.test.ts +0 -390
  164. package/src/__tests__/authManager.security.test.ts +0 -377
  165. package/src/__tests__/crossDomainAuth.test.ts +0 -116
  166. package/src/__tests__/establishDeviceRefreshSlot.test.ts +0 -221
  167. package/src/mixins/OxyServices.fedcm.ts +0 -1026
  168. package/src/mixins/OxyServices.redirect.ts +0 -122
  169. package/src/mixins/OxyServices.silent.ts +0 -272
  170. package/src/mixins/OxyServices.sso.ts +0 -261
  171. package/src/mixins/__tests__/constructorAuthWebUrl.test.ts +0 -85
  172. package/src/mixins/__tests__/fedcm.test.ts +0 -667
  173. package/src/mixins/__tests__/sessionBaseUrl.test.ts +0 -61
  174. package/src/mixins/__tests__/silent.test.ts +0 -102
  175. package/src/mixins/__tests__/sso.test.ts +0 -228
  176. package/src/utils/__tests__/consumeSsoReturn.test.ts +0 -816
  177. package/src/utils/__tests__/ssoBounce.test.ts +0 -219
  178. package/src/utils/__tests__/ssoEstablish.test.ts +0 -204
  179. package/src/utils/__tests__/ssoReturn.test.ts +0 -276
  180. package/src/utils/ssoEstablish.ts +0 -174
  181. package/src/utils/ssoReturn.ts +0 -389
@@ -1,390 +0,0 @@
1
- /**
2
- * AuthManager multi-account cookie-path regression tests.
3
- *
4
- * Locks in the four methods that route through the httpOnly
5
- * `oxy_rt_${authuser}` refresh cookies:
6
- *
7
- * - `restoreFromCookies()` — cold-boot restore of every device-local slot
8
- * via `POST /auth/refresh-all`. Picks active slot by persisted
9
- * `oxy_active_authuser`, falling back to lowest authuser.
10
- * - `switchAuthuser(n)` — mint a fresh access token for slot `n` via
11
- * `POST /auth/refresh?authuser=N`, plant it, persist active.
12
- * - `signOutAuthuser(n)` — `POST /auth/logout?authuser=N`, drop slot
13
- * locally, promote lowest remaining as active (or clear).
14
- * - `signOutAllViaCookies()` — `POST /auth/logout`, clear every slot,
15
- * clear persisted active.
16
- *
17
- * Storage rule: the cookie path NEVER reads or writes
18
- * `oxy_access_token` / `oxy_refresh_token` / `oxy_session`. Only the
19
- * integer slot index lives in `oxy_active_authuser` (not a secret).
20
- */
21
-
22
- import { AuthManager } from '../AuthManager';
23
- import type { StorageAdapter } from '../AuthManager';
24
- import type { OxyServices } from '../OxyServices';
25
- import type { RefreshAllResponse } from '../models/interfaces';
26
-
27
- const ACTIVE_AUTHUSER_KEY = 'oxy_active_authuser';
28
-
29
- function buildAccessToken(claims: Record<string, unknown>): string {
30
- const b64url = (value: string): string =>
31
- Buffer.from(value).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
32
- const header = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
33
- const payload = b64url(JSON.stringify(claims));
34
- return `${header}.${payload}.signature`;
35
- }
36
-
37
- class InMemoryStorage implements StorageAdapter {
38
- private store = new Map<string, string>();
39
- getItem(key: string): string | null { return this.store.get(key) ?? null; }
40
- setItem(key: string, value: string): void { this.store.set(key, value); }
41
- removeItem(key: string): void { this.store.delete(key); }
42
- has(key: string): boolean { return this.store.has(key); }
43
- raw(): Map<string, string> { return this.store; }
44
- }
45
-
46
- interface MockHttpService {
47
- setTokens: jest.Mock;
48
- setAuthRefreshHandler: jest.Mock;
49
- }
50
-
51
- interface MockServices {
52
- refreshAllSessions: jest.Mock<Promise<RefreshAllResponse>, []>;
53
- refreshTokenViaCookie: jest.Mock;
54
- logoutSessionByAuthuser: jest.Mock<Promise<void>, [number]>;
55
- logoutAllSessionsViaCookie: jest.Mock<Promise<void>, []>;
56
- httpService: MockHttpService;
57
- }
58
-
59
- function makeMockServices(): MockServices {
60
- return {
61
- refreshAllSessions: jest.fn(async (): Promise<RefreshAllResponse> => ({ accounts: [] })),
62
- refreshTokenViaCookie: jest.fn(),
63
- logoutSessionByAuthuser: jest.fn(async () => undefined),
64
- logoutAllSessionsViaCookie: jest.fn(async () => undefined),
65
- httpService: { setTokens: jest.fn(), setAuthRefreshHandler: jest.fn() },
66
- };
67
- }
68
-
69
- function makeManager(services: MockServices, storage: InMemoryStorage): AuthManager {
70
- const oxyServices = services as unknown as OxyServices;
71
- return new AuthManager(oxyServices, {
72
- storage,
73
- autoRefresh: false,
74
- crossTabSync: false,
75
- });
76
- }
77
-
78
- const TOKEN_SLOT_0 = buildAccessToken({ sessionId: 'sess-slot-0', userId: 'user-0', exp: 9999999999 });
79
- const TOKEN_SLOT_1 = buildAccessToken({ sessionId: 'sess-slot-1', userId: 'user-1', exp: 9999999999 });
80
-
81
- const TWO_ACCOUNTS: RefreshAllResponse = {
82
- accounts: [
83
- {
84
- authuser: 0,
85
- accessToken: TOKEN_SLOT_0,
86
- expiresAt: '2099-01-01T00:00:00.000Z',
87
- sessionId: 'sess-slot-0',
88
- user: { id: 'user-0', username: 'alice', name: { displayName: 'Alice' }, avatar: null, color: '#1abc9c' },
89
- },
90
- {
91
- authuser: 1,
92
- accessToken: TOKEN_SLOT_1,
93
- expiresAt: '2099-01-01T00:00:00.000Z',
94
- sessionId: 'sess-slot-1',
95
- user: { id: 'user-1', username: 'bob', name: { displayName: 'Bob' }, avatar: null, color: '#3498db' },
96
- },
97
- ],
98
- };
99
-
100
- describe('AuthManager.restoreFromCookies', () => {
101
- it('plants every account in the registry, picks lowest authuser when nothing is persisted, and persists the chosen slot', async () => {
102
- const services = makeMockServices();
103
- services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
104
- const storage = new InMemoryStorage();
105
- const manager = makeManager(services, storage);
106
-
107
- const result = await manager.restoreFromCookies();
108
-
109
- expect(result.accounts).toHaveLength(2);
110
- expect(result.activeAuthuser).toBe(0);
111
- expect(manager.getActiveAuthuser()).toBe(0);
112
-
113
- // Active slot's access token is planted on the HTTP client.
114
- expect(services.httpService.setTokens).toHaveBeenCalledWith(TOKEN_SLOT_0);
115
-
116
- // Persisted active authuser — the ONLY storage write of the cookie path.
117
- expect(storage.has(ACTIVE_AUTHUSER_KEY)).toBe(true);
118
- expect(storage.raw().get(ACTIVE_AUTHUSER_KEY)).toBe('0');
119
-
120
- // Sibling slot is in the registry but its token is NOT on the HTTP
121
- // client (that would clobber the active slot). Stays in-memory for a
122
- // future switchAuthuser hot-swap.
123
- const sibling = manager.getAccounts().find((a) => a.authuser === 1);
124
- expect(sibling?.accessToken).toBe(TOKEN_SLOT_1);
125
-
126
- // Legacy token storage MUST stay empty in the cookie path.
127
- expect(storage.has('oxy_access_token')).toBe(false);
128
- expect(storage.has('oxy_refresh_token')).toBe(false);
129
- expect(storage.has('oxy_session')).toBe(false);
130
- });
131
-
132
- it('honours persisted oxy_active_authuser when it matches a returned account', async () => {
133
- const services = makeMockServices();
134
- services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
135
- const storage = new InMemoryStorage();
136
- storage.setItem(ACTIVE_AUTHUSER_KEY, '1');
137
- const manager = makeManager(services, storage);
138
-
139
- const result = await manager.restoreFromCookies();
140
-
141
- expect(result.activeAuthuser).toBe(1);
142
- // Slot 1's token planted, not slot 0's.
143
- expect(services.httpService.setTokens).toHaveBeenCalledWith(TOKEN_SLOT_1);
144
- });
145
-
146
- it('falls back to lowest authuser when persisted slot is no longer returned', async () => {
147
- const services = makeMockServices();
148
- services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
149
- const storage = new InMemoryStorage();
150
- storage.setItem(ACTIVE_AUTHUSER_KEY, '7'); // stale: server doesn't return slot 7
151
- const manager = makeManager(services, storage);
152
-
153
- const result = await manager.restoreFromCookies();
154
-
155
- expect(result.activeAuthuser).toBe(0);
156
- // And the persisted value is corrected to the new active.
157
- expect(storage.raw().get(ACTIVE_AUTHUSER_KEY)).toBe('0');
158
- });
159
-
160
- it('returns an empty result without throwing on snapshot failure', async () => {
161
- const services = makeMockServices();
162
- services.refreshAllSessions.mockRejectedValueOnce(new TypeError('Failed to fetch'));
163
- const storage = new InMemoryStorage();
164
- const manager = makeManager(services, storage);
165
-
166
- const result = await manager.restoreFromCookies();
167
-
168
- expect(result.accounts).toEqual([]);
169
- expect(result.activeAuthuser).toBeNull();
170
- expect(services.httpService.setTokens).not.toHaveBeenCalled();
171
- });
172
-
173
- it('returns an empty result when no signed-in accounts on this device', async () => {
174
- const services = makeMockServices();
175
- // Default mock already returns `{ accounts: [] }`.
176
- const storage = new InMemoryStorage();
177
- const manager = makeManager(services, storage);
178
-
179
- const result = await manager.restoreFromCookies();
180
-
181
- expect(result.accounts).toEqual([]);
182
- expect(result.activeAuthuser).toBeNull();
183
- expect(manager.getActiveAccount()).toBeNull();
184
- expect(services.httpService.setTokens).not.toHaveBeenCalled();
185
- });
186
- });
187
-
188
- describe('AuthManager.switchAuthuser', () => {
189
- it('rotates a slot via refreshTokenViaCookie, plants its token, and persists the new active', async () => {
190
- const services = makeMockServices();
191
- services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
192
- services.refreshTokenViaCookie.mockResolvedValueOnce({
193
- accessToken: 'rotated-slot-1-token',
194
- expiresAt: '2099-01-01T00:00:00.000Z',
195
- authuser: 1,
196
- });
197
- const storage = new InMemoryStorage();
198
- const manager = makeManager(services, storage);
199
-
200
- await manager.restoreFromCookies();
201
- expect(manager.getActiveAuthuser()).toBe(0);
202
-
203
- const switched = await manager.switchAuthuser(1);
204
-
205
- expect(services.refreshTokenViaCookie).toHaveBeenCalledWith({ authuser: 1 });
206
- expect(switched.authuser).toBe(1);
207
- expect(switched.accessToken).toBe('rotated-slot-1-token');
208
-
209
- expect(manager.getActiveAuthuser()).toBe(1);
210
- expect(services.httpService.setTokens).toHaveBeenLastCalledWith('rotated-slot-1-token');
211
- expect(storage.raw().get(ACTIVE_AUTHUSER_KEY)).toBe('1');
212
-
213
- // The registry entry for slot 1 is updated to the rotated token; slot 0
214
- // is untouched (its access token is still valid).
215
- const slot1 = manager.getAccounts().find((a) => a.authuser === 1);
216
- expect(slot1?.accessToken).toBe('rotated-slot-1-token');
217
- const slot0 = manager.getAccounts().find((a) => a.authuser === 0);
218
- expect(slot0?.accessToken).toBe(TOKEN_SLOT_0);
219
- });
220
-
221
- it('throws and drops the slot when the cookie is missing/expired (refresh returns null)', async () => {
222
- const services = makeMockServices();
223
- services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
224
- services.refreshTokenViaCookie.mockResolvedValueOnce(null);
225
- const storage = new InMemoryStorage();
226
- const manager = makeManager(services, storage);
227
-
228
- await manager.restoreFromCookies();
229
-
230
- await expect(manager.switchAuthuser(1)).rejects.toThrow(/authuser=1/);
231
-
232
- // Slot 1 was removed from the registry; slot 0 remains and is still active.
233
- expect(manager.getAccounts().map((a) => a.authuser)).toEqual([0]);
234
- expect(manager.getActiveAuthuser()).toBe(0);
235
- });
236
- });
237
-
238
- describe('AuthManager.signOutAuthuser', () => {
239
- it('revokes the slot server-side, drops it from the registry, and promotes lowest remaining as active', async () => {
240
- const services = makeMockServices();
241
- services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
242
- const storage = new InMemoryStorage();
243
- const manager = makeManager(services, storage);
244
-
245
- await manager.restoreFromCookies();
246
- // Active = slot 0; sign it out.
247
- await manager.signOutAuthuser(0);
248
-
249
- expect(services.logoutSessionByAuthuser).toHaveBeenCalledWith(0);
250
- expect(manager.getAccounts().map((a) => a.authuser)).toEqual([1]);
251
- expect(manager.getActiveAuthuser()).toBe(1);
252
- // Slot 1's cached access token gets planted as the new active.
253
- expect(services.httpService.setTokens).toHaveBeenLastCalledWith(TOKEN_SLOT_1);
254
- expect(storage.raw().get(ACTIVE_AUTHUSER_KEY)).toBe('1');
255
- });
256
-
257
- it('clears state entirely when the last slot is signed out', async () => {
258
- const services = makeMockServices();
259
- services.refreshAllSessions.mockResolvedValueOnce({ accounts: [TWO_ACCOUNTS.accounts[0]] });
260
- const storage = new InMemoryStorage();
261
- const manager = makeManager(services, storage);
262
-
263
- await manager.restoreFromCookies();
264
- await manager.signOutAuthuser(0);
265
-
266
- expect(manager.getAccounts()).toEqual([]);
267
- expect(manager.getActiveAuthuser()).toBeNull();
268
- expect(manager.getActiveAccount()).toBeNull();
269
- expect(services.httpService.setTokens).toHaveBeenLastCalledWith('');
270
- expect(storage.has(ACTIVE_AUTHUSER_KEY)).toBe(false);
271
- });
272
-
273
- it('signs out a non-active slot without disturbing the active one', async () => {
274
- const services = makeMockServices();
275
- services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
276
- const storage = new InMemoryStorage();
277
- const manager = makeManager(services, storage);
278
-
279
- await manager.restoreFromCookies();
280
- expect(manager.getActiveAuthuser()).toBe(0);
281
- services.httpService.setTokens.mockClear();
282
-
283
- await manager.signOutAuthuser(1);
284
-
285
- expect(services.logoutSessionByAuthuser).toHaveBeenCalledWith(1);
286
- expect(manager.getAccounts().map((a) => a.authuser)).toEqual([0]);
287
- expect(manager.getActiveAuthuser()).toBe(0);
288
- // Active slot's token must NOT be re-planted (it was never inactive).
289
- expect(services.httpService.setTokens).not.toHaveBeenCalled();
290
- });
291
- });
292
-
293
- describe('AuthManager.signOutAllViaCookies', () => {
294
- it('clears every slot, the HTTP client token, and the persisted active authuser', async () => {
295
- const services = makeMockServices();
296
- services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
297
- const storage = new InMemoryStorage();
298
- const manager = makeManager(services, storage);
299
-
300
- await manager.restoreFromCookies();
301
- await manager.signOutAllViaCookies();
302
-
303
- expect(services.logoutAllSessionsViaCookie).toHaveBeenCalledTimes(1);
304
- expect(manager.getAccounts()).toEqual([]);
305
- expect(manager.getActiveAuthuser()).toBeNull();
306
- expect(services.httpService.setTokens).toHaveBeenLastCalledWith('');
307
- expect(storage.has(ACTIVE_AUTHUSER_KEY)).toBe(false);
308
- });
309
- });
310
-
311
- describe('AuthManager.initialize', () => {
312
- it('returns the active user from restoreFromCookies and never touches localStorage tokens', async () => {
313
- const services = makeMockServices();
314
- services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
315
- const storage = new InMemoryStorage();
316
- const manager = makeManager(services, storage);
317
-
318
- const user = await manager.initialize();
319
-
320
- expect(user?.id).toBe('user-0');
321
- expect(user?.username).toBe('alice');
322
- expect(storage.has('oxy_access_token')).toBe(false);
323
- expect(storage.has('oxy_session')).toBe(false);
324
- expect(storage.has('oxy_user')).toBe(false);
325
- });
326
-
327
- it('returns null when no cookies are restored', async () => {
328
- const services = makeMockServices();
329
- // Default `{ accounts: [] }`.
330
- const storage = new InMemoryStorage();
331
- storage.setItem('oxy_access_token', 'stale-storage-token');
332
- storage.setItem('oxy_user', JSON.stringify({ id: 'stale', username: 'stale' }));
333
- const manager = makeManager(services, storage);
334
-
335
- const user = await manager.initialize();
336
-
337
- expect(user).toBeNull();
338
- // Stale storage token material is ignored.
339
- expect(services.httpService.setTokens).not.toHaveBeenCalled();
340
- });
341
- });
342
-
343
- describe('AuthManager.getAccessToken', () => {
344
- it('returns the in-memory token after a cold-boot cookie restore even though storage holds no token', async () => {
345
- // Regression: the cookie restore path plants the active token ONLY in memory
346
- // (`_lastKnownAccessToken` + httpService) and intentionally NEVER writes
347
- // `oxy_access_token`. getAccessToken() must fall back to that in-memory token,
348
- // otherwise standalone API clients reading `authManager.getAccessToken()` send
349
- // no Authorization header → 401 on every authed endpoint after every reload.
350
- const services = makeMockServices();
351
- services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
352
- const storage = new InMemoryStorage();
353
- const manager = makeManager(services, storage);
354
-
355
- await manager.restoreFromCookies();
356
-
357
- // Storage was never touched for the access token.
358
- expect(storage.has('oxy_access_token')).toBe(false);
359
-
360
- // getAccessToken still resolves the active slot's token from memory.
361
- const token = await manager.getAccessToken();
362
- expect(token).toBe(TOKEN_SLOT_0);
363
- });
364
-
365
- it('returns null when neither storage nor the in-memory token is present', async () => {
366
- const services = makeMockServices();
367
- const storage = new InMemoryStorage();
368
- const manager = makeManager(services, storage);
369
-
370
- const token = await manager.getAccessToken();
371
- expect(token).toBeNull();
372
- });
373
-
374
- it('ignores a retired storage token when an in-memory cookie-restored token is present', async () => {
375
- const services = makeMockServices();
376
- services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
377
- const storage = new InMemoryStorage();
378
- const manager = makeManager(services, storage);
379
-
380
- // After restore the in-memory token is TOKEN_SLOT_0.
381
- await manager.restoreFromCookies();
382
-
383
- // Simulate stale token material from a retired storage path. Memory wins.
384
- const STORAGE_TOKEN = buildAccessToken({ sessionId: 'sess-storage', userId: 'user-storage', exp: 9999999999 });
385
- storage.setItem('oxy_access_token', STORAGE_TOKEN);
386
-
387
- const token = await manager.getAccessToken();
388
- expect(token).toBe(TOKEN_SLOT_0);
389
- });
390
- });