@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,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
- });