@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +6 -3
- package/dist/cjs/OxyServices.base.js +7 -102
- package/dist/cjs/boot/coldBootV2.js +350 -0
- package/dist/cjs/boot/deviceBootReturn.js +152 -0
- package/dist/cjs/crypto/keyManager.js +95 -0
- package/dist/cjs/i18n/locales/en-US.json +13 -1
- package/dist/cjs/i18n/locales/es-ES.json +13 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +13 -1
- package/dist/cjs/i18n/locales/locales/es-ES.json +13 -1
- package/dist/cjs/index.js +47 -44
- package/dist/cjs/mixins/OxyServices.accounts.js +6 -13
- package/dist/cjs/mixins/OxyServices.auth.js +66 -201
- package/dist/cjs/mixins/OxyServices.authorizedApps.js +38 -0
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +119 -0
- package/dist/cjs/mixins/index.js +13 -17
- package/dist/cjs/session/SessionClient.js +40 -1
- package/dist/cjs/session/authStateStore.js +284 -0
- package/dist/cjs/session/refresh.js +264 -0
- package/dist/cjs/utils/accountUtils.js +1 -55
- package/dist/cjs/utils/authWebUrl.js +6 -15
- package/dist/cjs/utils/fapiAutoDetect.js +16 -64
- package/dist/cjs/utils/platform.js +19 -0
- package/dist/cjs/utils/ssoBounce.js +15 -362
- package/dist/cjs/utils/validationUtils.js +57 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +6 -3
- package/dist/esm/OxyServices.base.js +7 -102
- package/dist/esm/boot/coldBootV2.js +344 -0
- package/dist/esm/boot/deviceBootReturn.js +146 -0
- package/dist/esm/crypto/keyManager.js +95 -0
- package/dist/esm/i18n/locales/en-US.json +13 -1
- package/dist/esm/i18n/locales/es-ES.json +13 -1
- package/dist/esm/i18n/locales/locales/en-US.json +13 -1
- package/dist/esm/i18n/locales/locales/es-ES.json +13 -1
- package/dist/esm/index.js +28 -18
- package/dist/esm/mixins/OxyServices.accounts.js +6 -13
- package/dist/esm/mixins/OxyServices.auth.js +66 -201
- package/dist/esm/mixins/OxyServices.authorizedApps.js +35 -0
- package/dist/esm/mixins/OxyServices.deviceBoot.js +116 -0
- package/dist/esm/mixins/index.js +13 -17
- package/dist/esm/session/SessionClient.js +40 -1
- package/dist/esm/session/authStateStore.js +278 -0
- package/dist/esm/session/refresh.js +257 -0
- package/dist/esm/utils/accountUtils.js +0 -53
- package/dist/esm/utils/authWebUrl.js +6 -14
- package/dist/esm/utils/fapiAutoDetect.js +16 -63
- package/dist/esm/utils/platform.js +18 -0
- package/dist/esm/utils/ssoBounce.js +14 -345
- package/dist/esm/utils/validationUtils.js +56 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +13 -0
- package/dist/types/OxyServices.base.d.ts +0 -52
- package/dist/types/OxyServices.d.ts +0 -25
- package/dist/types/boot/coldBootV2.d.ts +76 -0
- package/dist/types/boot/deviceBootReturn.d.ts +83 -0
- package/dist/types/crypto/keyManager.d.ts +21 -0
- package/dist/types/index.d.ts +15 -21
- package/dist/types/mixins/OxyServices.accounts.d.ts +0 -2
- package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
- package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
- package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
- package/dist/types/mixins/OxyServices.auth.d.ts +35 -77
- package/dist/types/mixins/{OxyServices.redirect.d.ts → OxyServices.authorizedApps.d.ts} +35 -33
- package/dist/types/mixins/OxyServices.civic.d.ts +0 -2
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
- package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +110 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
- package/dist/types/mixins/OxyServices.features.d.ts +0 -2
- package/dist/types/mixins/OxyServices.identity.d.ts +0 -2
- package/dist/types/mixins/OxyServices.language.d.ts +0 -2
- package/dist/types/mixins/OxyServices.links.d.ts +0 -2
- package/dist/types/mixins/OxyServices.location.d.ts +0 -2
- package/dist/types/mixins/OxyServices.nodes.d.ts +0 -2
- package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
- package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
- package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
- package/dist/types/mixins/OxyServices.security.d.ts +0 -2
- package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
- package/dist/types/mixins/OxyServices.user.d.ts +0 -2
- package/dist/types/mixins/OxyServices.utility.d.ts +0 -2
- package/dist/types/mixins/index.d.ts +6 -9
- package/dist/types/models/interfaces.d.ts +0 -67
- package/dist/types/session/SessionClient.d.ts +25 -0
- package/dist/types/session/authStateStore.d.ts +119 -0
- package/dist/types/session/refresh.d.ts +93 -0
- package/dist/types/utils/accountUtils.d.ts +0 -14
- package/dist/types/utils/authWebUrl.d.ts +6 -12
- package/dist/types/utils/fapiAutoDetect.d.ts +15 -38
- package/dist/types/utils/platform.d.ts +14 -0
- package/dist/types/utils/ssoBounce.d.ts +14 -280
- package/dist/types/utils/validationUtils.d.ts +15 -0
- package/package.json +2 -2
- package/src/HttpService.ts +19 -3
- package/src/OxyServices.base.ts +7 -112
- package/src/OxyServices.ts +0 -38
- package/src/boot/__tests__/coldBootV2.test.ts +317 -0
- package/src/boot/__tests__/deviceBootReturn.test.ts +158 -0
- package/src/boot/coldBootV2.ts +426 -0
- package/src/boot/deviceBootReturn.ts +195 -0
- package/src/crypto/__tests__/sharedDeviceToken.test.ts +24 -0
- package/src/crypto/keyManager.ts +101 -0
- package/src/i18n/locales/en-US.json +13 -1
- package/src/i18n/locales/es-ES.json +13 -1
- package/src/index.ts +74 -65
- package/src/mixins/OxyServices.accounts.ts +6 -13
- package/src/mixins/OxyServices.auth.ts +78 -253
- package/src/mixins/OxyServices.authorizedApps.ts +75 -0
- package/src/mixins/OxyServices.deviceBoot.ts +146 -0
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +107 -0
- package/src/mixins/__tests__/accounts.test.ts +17 -44
- package/src/mixins/__tests__/authorizedApps.test.ts +63 -0
- package/src/mixins/__tests__/passwordSignIn.test.ts +91 -0
- package/src/mixins/index.ts +16 -22
- package/src/models/interfaces.ts +0 -79
- package/src/session/SessionClient.ts +52 -1
- package/src/session/__tests__/SessionClient.additive.test.ts +92 -0
- package/src/session/__tests__/SessionClient.rest.test.ts +25 -0
- package/src/session/__tests__/SessionClient.state.test.ts +18 -5
- package/src/session/__tests__/authStateStore.test.ts +209 -0
- package/src/session/__tests__/refresh.test.ts +256 -0
- package/src/session/authStateStore.ts +335 -0
- package/src/session/refresh.ts +334 -0
- package/src/utils/__tests__/authWebUrl.test.ts +5 -29
- package/src/utils/__tests__/fapiAutoDetect.test.ts +5 -126
- package/src/utils/__tests__/validationUtils.test.ts +30 -0
- package/src/utils/accountUtils.ts +0 -62
- package/src/utils/authWebUrl.ts +6 -15
- package/src/utils/fapiAutoDetect.ts +16 -60
- package/src/utils/platform.ts +21 -0
- package/src/utils/ssoBounce.ts +14 -393
- package/src/utils/validationUtils.ts +62 -0
- package/dist/cjs/AuthManager.js +0 -1110
- package/dist/cjs/AuthManagerTypes.js +0 -13
- package/dist/cjs/CrossDomainAuth.js +0 -206
- package/dist/cjs/mixins/OxyServices.fedcm.js +0 -823
- package/dist/cjs/mixins/OxyServices.redirect.js +0 -95
- package/dist/cjs/mixins/OxyServices.silent.js +0 -204
- package/dist/cjs/mixins/OxyServices.sso.js +0 -208
- package/dist/cjs/utils/ssoEstablish.js +0 -110
- package/dist/cjs/utils/ssoReturn.js +0 -275
- package/dist/esm/AuthManager.js +0 -1105
- package/dist/esm/AuthManagerTypes.js +0 -12
- package/dist/esm/CrossDomainAuth.js +0 -201
- package/dist/esm/mixins/OxyServices.fedcm.js +0 -821
- package/dist/esm/mixins/OxyServices.redirect.js +0 -92
- package/dist/esm/mixins/OxyServices.silent.js +0 -202
- package/dist/esm/mixins/OxyServices.sso.js +0 -204
- package/dist/esm/utils/ssoEstablish.js +0 -107
- package/dist/esm/utils/ssoReturn.js +0 -271
- package/dist/types/AuthManager.d.ts +0 -380
- package/dist/types/AuthManagerTypes.d.ts +0 -81
- package/dist/types/CrossDomainAuth.d.ts +0 -164
- package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -331
- package/dist/types/mixins/OxyServices.silent.d.ts +0 -132
- package/dist/types/mixins/OxyServices.sso.d.ts +0 -138
- package/dist/types/utils/ssoEstablish.d.ts +0 -85
- package/dist/types/utils/ssoReturn.d.ts +0 -156
- package/src/AuthManager.ts +0 -1269
- package/src/AuthManagerTypes.ts +0 -86
- package/src/CrossDomainAuth.ts +0 -243
- package/src/__tests__/authManager.cookiePath.test.ts +0 -390
- package/src/__tests__/authManager.security.test.ts +0 -377
- package/src/__tests__/crossDomainAuth.test.ts +0 -116
- package/src/__tests__/establishDeviceRefreshSlot.test.ts +0 -221
- package/src/mixins/OxyServices.fedcm.ts +0 -1026
- package/src/mixins/OxyServices.redirect.ts +0 -122
- package/src/mixins/OxyServices.silent.ts +0 -272
- package/src/mixins/OxyServices.sso.ts +0 -261
- package/src/mixins/__tests__/constructorAuthWebUrl.test.ts +0 -85
- package/src/mixins/__tests__/fedcm.test.ts +0 -667
- package/src/mixins/__tests__/sessionBaseUrl.test.ts +0 -61
- package/src/mixins/__tests__/silent.test.ts +0 -102
- package/src/mixins/__tests__/sso.test.ts +0 -228
- package/src/utils/__tests__/consumeSsoReturn.test.ts +0 -816
- package/src/utils/__tests__/ssoBounce.test.ts +0 -219
- package/src/utils/__tests__/ssoEstablish.test.ts +0 -204
- package/src/utils/__tests__/ssoReturn.test.ts +0 -276
- package/src/utils/ssoEstablish.ts +0 -174
- 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
|
-
});
|