@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,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `OxyServices.getSessionBaseUrl()` resolution tests.
|
|
3
|
-
*
|
|
4
|
-
* Per the 2026 session architecture (docs/SESSION-ARCHITECTURE.md), every app
|
|
5
|
-
* keeps its OWN first-party session on its OWN domain. `getSessionBaseUrl()`
|
|
6
|
-
* is the configurable base URL the SDK's first-party session/refresh calls will
|
|
7
|
-
* target in a later phase:
|
|
8
|
-
* - non-`oxy.so` apps point `sessionBaseUrl` at their own same-site backend
|
|
9
|
-
* (e.g. `https://api.mention.earth`);
|
|
10
|
-
* - `*.oxy.so` apps leave it unset so it falls back to `baseURL`
|
|
11
|
-
* (`https://api.oxy.so`) — their behavior is unchanged.
|
|
12
|
-
*
|
|
13
|
-
* This phase is additive: the getter only surfaces configuration. It must NOT
|
|
14
|
-
* mutate token/auth state and must NOT alter `getBaseURL()`.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { OxyServices } from '../../OxyServices';
|
|
18
|
-
|
|
19
|
-
describe('OxyServices.getSessionBaseUrl', () => {
|
|
20
|
-
it('falls back to baseURL when sessionBaseUrl is not configured', () => {
|
|
21
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
22
|
-
|
|
23
|
-
expect(oxy.getSessionBaseUrl()).toBe('https://api.oxy.so');
|
|
24
|
-
// Must equal the API base URL exactly — no divergence for *.oxy.so apps.
|
|
25
|
-
expect(oxy.getSessionBaseUrl()).toBe(oxy.getBaseURL());
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('returns the configured sessionBaseUrl when provided', () => {
|
|
29
|
-
const oxy = new OxyServices({
|
|
30
|
-
baseURL: 'https://api.oxy.so',
|
|
31
|
-
sessionBaseUrl: 'https://api.mention.earth',
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
expect(oxy.getSessionBaseUrl()).toBe('https://api.mention.earth');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('does not change the API base URL when sessionBaseUrl differs', () => {
|
|
38
|
-
const oxy = new OxyServices({
|
|
39
|
-
baseURL: 'https://api.oxy.so',
|
|
40
|
-
sessionBaseUrl: 'https://api.mention.earth',
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// getBaseURL (the HTTP client's request base) is independent of the
|
|
44
|
-
// session base — only the latter is overridden by config.
|
|
45
|
-
expect(oxy.getBaseURL()).toBe('https://api.oxy.so');
|
|
46
|
-
expect(oxy.getSessionBaseUrl()).not.toBe(oxy.getBaseURL());
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('is a pure read — it does not touch token/auth state', () => {
|
|
50
|
-
const oxy = new OxyServices({
|
|
51
|
-
baseURL: 'https://api.oxy.so',
|
|
52
|
-
sessionBaseUrl: 'https://api.mention.earth',
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
expect(oxy.hasValidToken()).toBe(false);
|
|
56
|
-
// Resolving the session base must not plant or clear any token.
|
|
57
|
-
oxy.getSessionBaseUrl();
|
|
58
|
-
expect(oxy.hasValidToken()).toBe(false);
|
|
59
|
-
expect(oxy.getAccessToken()).toBeNull();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Silent iframe auth tests.
|
|
3
|
-
*
|
|
4
|
-
* The cross-domain durable-restore iframe (`/auth/silent` at the per-apex host)
|
|
5
|
-
* posts a message on success. On a failed load, it never posts, so
|
|
6
|
-
* `waitForIframeAuth` must resolve `null` immediately instead of waiting for
|
|
7
|
-
* the full timeout.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { OxyServices } from '../../OxyServices';
|
|
11
|
-
|
|
12
|
-
const ORIGIN = 'https://mention.earth';
|
|
13
|
-
|
|
14
|
-
function installBrowserGlobals(options: {
|
|
15
|
-
postMessageDispatcher?: { current: ((event: { origin: string; data: unknown }) => void) | null };
|
|
16
|
-
} = {}): void {
|
|
17
|
-
const store = new Map<string, string>();
|
|
18
|
-
const sessionStorageStub = {
|
|
19
|
-
getItem: (k: string) => (store.has(k) ? (store.get(k) as string) : null),
|
|
20
|
-
setItem: (k: string, v: string) => { store.set(k, v); },
|
|
21
|
-
removeItem: (k: string) => { store.delete(k); },
|
|
22
|
-
};
|
|
23
|
-
const messageHandlers: Array<(event: { origin: string; data: unknown }) => void> = [];
|
|
24
|
-
const win = {
|
|
25
|
-
location: { origin: ORIGIN, hostname: 'mention.earth' },
|
|
26
|
-
sessionStorage: sessionStorageStub,
|
|
27
|
-
addEventListener: (event: string, handler: (e: { origin: string; data: unknown }) => void) => {
|
|
28
|
-
if (event === 'message') {
|
|
29
|
-
messageHandlers.push(handler);
|
|
30
|
-
if (options.postMessageDispatcher) {
|
|
31
|
-
options.postMessageDispatcher.current = handler;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
removeEventListener: (event: string, handler: (e: { origin: string; data: unknown }) => void) => {
|
|
36
|
-
if (event === 'message') {
|
|
37
|
-
const idx = messageHandlers.indexOf(handler);
|
|
38
|
-
if (idx >= 0) messageHandlers.splice(idx, 1);
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
(globalThis as unknown as { window: unknown }).window = win;
|
|
43
|
-
(globalThis as unknown as { sessionStorage: unknown }).sessionStorage = sessionStorageStub;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function clearBrowserGlobals(): void {
|
|
47
|
-
for (const key of ['window', 'sessionStorage'] as const) {
|
|
48
|
-
delete (globalThis as Record<string, unknown>)[key];
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface FakeIframe {
|
|
53
|
-
onerror: ((this: unknown, ...args: unknown[]) => unknown) | null;
|
|
54
|
-
onabort: ((this: unknown, ...args: unknown[]) => unknown) | null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
describe('OxyServices waitForIframeAuth fail-fast on iframe load error', () => {
|
|
58
|
-
afterEach(() => {
|
|
59
|
-
clearBrowserGlobals();
|
|
60
|
-
jest.restoreAllMocks();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('resolves null immediately when the iframe fires onerror', async () => {
|
|
64
|
-
installBrowserGlobals();
|
|
65
|
-
|
|
66
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
67
|
-
const iframe: FakeIframe = { onerror: null, onabort: null };
|
|
68
|
-
|
|
69
|
-
const settled = oxy.waitForIframeAuth(
|
|
70
|
-
iframe as unknown as HTMLIFrameElement,
|
|
71
|
-
100000,
|
|
72
|
-
'https://auth.mention.earth',
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
await Promise.resolve();
|
|
76
|
-
expect(typeof iframe.onerror).toBe('function');
|
|
77
|
-
iframe.onerror?.call(iframe);
|
|
78
|
-
|
|
79
|
-
await expect(settled).resolves.toBeNull();
|
|
80
|
-
expect(iframe.onerror).toBeNull();
|
|
81
|
-
expect(iframe.onabort).toBeNull();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('resolves null immediately when the iframe fires onabort', async () => {
|
|
85
|
-
installBrowserGlobals();
|
|
86
|
-
|
|
87
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
88
|
-
const iframe: FakeIframe = { onerror: null, onabort: null };
|
|
89
|
-
|
|
90
|
-
const settled = oxy.waitForIframeAuth(
|
|
91
|
-
iframe as unknown as HTMLIFrameElement,
|
|
92
|
-
100000,
|
|
93
|
-
'https://auth.mention.earth',
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
await Promise.resolve();
|
|
97
|
-
expect(typeof iframe.onabort).toBe('function');
|
|
98
|
-
iframe.onabort?.call(iframe);
|
|
99
|
-
|
|
100
|
-
await expect(settled).resolves.toBeNull();
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `OxyServices.exchangeSsoCode` / `generateSsoState` — central SSO client.
|
|
3
|
-
*
|
|
4
|
-
* `exchangeSsoCode(code)` POSTs the opaque single-use code to the session base
|
|
5
|
-
* URL (`getSessionBaseUrl()` — i.e. `api.oxy.so` by default) at `/sso/exchange`
|
|
6
|
-
* with NO credentials, then plants the returned access token via
|
|
7
|
-
* `httpService.setTokens(...)` — mirroring `exchangeIdTokenForSession` /
|
|
8
|
-
* `verifyChallenge`. NO token/JWT ever travels in the URL; the real token only
|
|
9
|
-
* arrives in this exchange response body.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { OxyServices } from '../../OxyServices';
|
|
13
|
-
import { generateSsoState } from '../OxyServices.sso';
|
|
14
|
-
import { ssoStateKey } from '../../utils/ssoBounce';
|
|
15
|
-
|
|
16
|
-
interface FetchCall {
|
|
17
|
-
url: string;
|
|
18
|
-
init: RequestInit;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function mockFetchOnce(body: unknown, ok = true, status = 200): { calls: FetchCall[] } {
|
|
22
|
-
const calls: FetchCall[] = [];
|
|
23
|
-
const fetchMock = jest.fn(async (url: string, init: RequestInit) => {
|
|
24
|
-
calls.push({ url, init });
|
|
25
|
-
return {
|
|
26
|
-
ok,
|
|
27
|
-
status,
|
|
28
|
-
json: async () => body,
|
|
29
|
-
} as unknown as Response;
|
|
30
|
-
});
|
|
31
|
-
(globalThis as unknown as { fetch: typeof fetch }).fetch = fetchMock as unknown as typeof fetch;
|
|
32
|
-
return { calls };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const VALID_BODY = {
|
|
36
|
-
accessToken: 'access_sso',
|
|
37
|
-
sessionId: 'sess_sso',
|
|
38
|
-
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
39
|
-
authuser: 0,
|
|
40
|
-
user: {
|
|
41
|
-
id: 'user_sso',
|
|
42
|
-
username: 'ssouser',
|
|
43
|
-
name: { displayName: 'SSO User', first: 'SSO', last: 'User', full: 'SSO User' },
|
|
44
|
-
avatar: 'file_1',
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
describe('OxyServices.exchangeSsoCode', () => {
|
|
49
|
-
const originalFetch = globalThis.fetch;
|
|
50
|
-
|
|
51
|
-
afterEach(() => {
|
|
52
|
-
(globalThis as unknown as { fetch: typeof fetch }).fetch = originalFetch;
|
|
53
|
-
jest.restoreAllMocks();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('POSTs the code to the API base /sso/exchange with no credentials', async () => {
|
|
57
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
58
|
-
const { calls } = mockFetchOnce(VALID_BODY);
|
|
59
|
-
|
|
60
|
-
await oxy.exchangeSsoCode('opaque-code-123');
|
|
61
|
-
|
|
62
|
-
expect(calls).toHaveLength(1);
|
|
63
|
-
expect(calls[0].url).toBe('https://api.oxy.so/sso/exchange');
|
|
64
|
-
expect(calls[0].init.method).toBe('POST');
|
|
65
|
-
expect(calls[0].init.credentials).toBe('omit');
|
|
66
|
-
expect(JSON.parse(String(calls[0].init.body))).toEqual({ code: 'opaque-code-123' });
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('targets the configured sessionBaseUrl when set', async () => {
|
|
70
|
-
const oxy = new OxyServices({
|
|
71
|
-
baseURL: 'https://api.oxy.so',
|
|
72
|
-
sessionBaseUrl: 'https://api.mention.earth',
|
|
73
|
-
});
|
|
74
|
-
const { calls } = mockFetchOnce(VALID_BODY);
|
|
75
|
-
|
|
76
|
-
await oxy.exchangeSsoCode('opaque-code-123');
|
|
77
|
-
|
|
78
|
-
expect(calls[0].url).toBe('https://api.mention.earth/sso/exchange');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('plants the access token via setTokens and returns the session', async () => {
|
|
82
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
83
|
-
expect(oxy.hasValidToken()).toBe(false);
|
|
84
|
-
mockFetchOnce(VALID_BODY);
|
|
85
|
-
|
|
86
|
-
const session = await oxy.exchangeSsoCode('opaque-code-123');
|
|
87
|
-
|
|
88
|
-
expect(oxy.hasValidToken()).toBe(true);
|
|
89
|
-
expect(oxy.getAccessToken()).toBe('access_sso');
|
|
90
|
-
expect(session.sessionId).toBe('sess_sso');
|
|
91
|
-
expect(session.accessToken).toBe('access_sso');
|
|
92
|
-
expect(session.user).toEqual({
|
|
93
|
-
id: 'user_sso',
|
|
94
|
-
username: 'ssouser',
|
|
95
|
-
name: { displayName: 'SSO User', first: 'SSO', last: 'User', full: 'SSO User' },
|
|
96
|
-
avatar: 'file_1',
|
|
97
|
-
});
|
|
98
|
-
expect(session.expiresAt).toBe(VALID_BODY.expiresAt);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('maps a user delivered as _id', async () => {
|
|
102
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
103
|
-
mockFetchOnce({
|
|
104
|
-
accessToken: 'access_sso',
|
|
105
|
-
sessionId: 'sess_sso',
|
|
106
|
-
user: { _id: 'mongo_id', username: 'ssouser', name: { displayName: 'SSO User' } },
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const session = await oxy.exchangeSsoCode('opaque-code-123');
|
|
110
|
-
|
|
111
|
-
expect(session.user.id).toBe('mongo_id');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('rejects an empty code without calling fetch', async () => {
|
|
115
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
116
|
-
const { calls } = mockFetchOnce(VALID_BODY);
|
|
117
|
-
|
|
118
|
-
await expect(oxy.exchangeSsoCode('')).rejects.toThrow();
|
|
119
|
-
expect(calls).toHaveLength(0);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('rejects a browser SSO exchange when stored state is not echoed', async () => {
|
|
123
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
124
|
-
const { calls } = mockFetchOnce(VALID_BODY);
|
|
125
|
-
const storage = new Map([[ssoStateKey('https://rp.example'), 'expected-state']]);
|
|
126
|
-
const previousWindow = (globalThis as unknown as { window?: unknown }).window;
|
|
127
|
-
|
|
128
|
-
Object.defineProperty(globalThis, 'window', {
|
|
129
|
-
configurable: true,
|
|
130
|
-
value: {
|
|
131
|
-
location: { origin: 'https://rp.example' },
|
|
132
|
-
sessionStorage: {
|
|
133
|
-
getItem: (key: string) => storage.get(key) ?? null,
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
await expect(oxy.exchangeSsoCode('opaque-code-123', 'attacker-state')).rejects.toThrow(
|
|
140
|
-
'SSO exchange state mismatch',
|
|
141
|
-
);
|
|
142
|
-
expect(calls).toHaveLength(0);
|
|
143
|
-
expect(oxy.hasValidToken()).toBe(false);
|
|
144
|
-
} finally {
|
|
145
|
-
Object.defineProperty(globalThis, 'window', {
|
|
146
|
-
configurable: true,
|
|
147
|
-
value: previousWindow,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('throws and does not plant a token on a non-2xx response', async () => {
|
|
153
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
154
|
-
mockFetchOnce({ error: 'invalid_code' }, false, 400);
|
|
155
|
-
|
|
156
|
-
await expect(oxy.exchangeSsoCode('bad-code')).rejects.toThrow();
|
|
157
|
-
expect(oxy.hasValidToken()).toBe(false);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('throws when the response carries no access token', async () => {
|
|
161
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
162
|
-
mockFetchOnce({ sessionId: 'sess_sso', user: { id: 'u', username: 'x' } });
|
|
163
|
-
|
|
164
|
-
await expect(oxy.exchangeSsoCode('opaque-code-123')).rejects.toThrow();
|
|
165
|
-
expect(oxy.hasValidToken()).toBe(false);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe('OxyServices.requestSsoEstablishUrl', () => {
|
|
170
|
-
const ESTABLISH_URL =
|
|
171
|
-
'https://auth.oxy.so/sso/establish?et=jwt&return_to=https%3A%2F%2Faccounts.oxy.so%2F__oxy%2Fsso-callback&state=s';
|
|
172
|
-
|
|
173
|
-
it('POSTs origin + state to /sso/establish-token (bearer, cache-free) and returns the URL', async () => {
|
|
174
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
175
|
-
const spy = jest
|
|
176
|
-
.spyOn(oxy, 'makeRequest')
|
|
177
|
-
.mockResolvedValue({ establishUrl: ESTABLISH_URL } as never);
|
|
178
|
-
|
|
179
|
-
const result = await oxy.requestSsoEstablishUrl('https://accounts.oxy.so', 's');
|
|
180
|
-
|
|
181
|
-
expect(result).toEqual({ establishUrl: ESTABLISH_URL });
|
|
182
|
-
expect(spy).toHaveBeenCalledWith(
|
|
183
|
-
'POST',
|
|
184
|
-
'/sso/establish-token',
|
|
185
|
-
{ origin: 'https://accounts.oxy.so', state: 's' },
|
|
186
|
-
{ cache: false },
|
|
187
|
-
);
|
|
188
|
-
spy.mockRestore();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('rejects an empty origin or state without calling the API', async () => {
|
|
192
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
193
|
-
const spy = jest.spyOn(oxy, 'makeRequest');
|
|
194
|
-
|
|
195
|
-
await expect(oxy.requestSsoEstablishUrl('', 's')).rejects.toThrow();
|
|
196
|
-
await expect(oxy.requestSsoEstablishUrl('https://accounts.oxy.so', '')).rejects.toThrow();
|
|
197
|
-
expect(spy).not.toHaveBeenCalled();
|
|
198
|
-
spy.mockRestore();
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('throws when the server returns no establishUrl', async () => {
|
|
202
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
203
|
-
const spy = jest.spyOn(oxy, 'makeRequest').mockResolvedValue({} as never);
|
|
204
|
-
|
|
205
|
-
await expect(oxy.requestSsoEstablishUrl('https://accounts.oxy.so', 's')).rejects.toThrow();
|
|
206
|
-
spy.mockRestore();
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe('generateSsoState', () => {
|
|
211
|
-
it('returns a non-empty unique string (module-level helper)', () => {
|
|
212
|
-
const a = generateSsoState();
|
|
213
|
-
const b = generateSsoState();
|
|
214
|
-
|
|
215
|
-
expect(typeof a).toBe('string');
|
|
216
|
-
expect(a.length).toBeGreaterThan(0);
|
|
217
|
-
expect(a).not.toBe(b);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('is also reachable as an instance method delegating to generateState', () => {
|
|
221
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
222
|
-
|
|
223
|
-
const state = oxy.generateSsoState();
|
|
224
|
-
|
|
225
|
-
expect(typeof state).toBe('string');
|
|
226
|
-
expect(state.length).toBeGreaterThan(0);
|
|
227
|
-
});
|
|
228
|
-
});
|