@oxyhq/core 5.4.3 → 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 (186) 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 -43
  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 +44 -2
  18. package/dist/cjs/session/authStateStore.js +284 -0
  19. package/dist/cjs/session/createSessionClient.js +8 -2
  20. package/dist/cjs/session/refresh.js +264 -0
  21. package/dist/cjs/utils/accountUtils.js +1 -55
  22. package/dist/cjs/utils/authWebUrl.js +6 -15
  23. package/dist/cjs/utils/fapiAutoDetect.js +16 -64
  24. package/dist/cjs/utils/platform.js +19 -0
  25. package/dist/cjs/utils/ssoBounce.js +15 -340
  26. package/dist/cjs/utils/validationUtils.js +57 -0
  27. package/dist/esm/.tsbuildinfo +1 -1
  28. package/dist/esm/HttpService.js +6 -3
  29. package/dist/esm/OxyServices.base.js +7 -102
  30. package/dist/esm/boot/coldBootV2.js +344 -0
  31. package/dist/esm/boot/deviceBootReturn.js +146 -0
  32. package/dist/esm/crypto/keyManager.js +95 -0
  33. package/dist/esm/i18n/locales/en-US.json +13 -1
  34. package/dist/esm/i18n/locales/es-ES.json +13 -1
  35. package/dist/esm/i18n/locales/locales/en-US.json +13 -1
  36. package/dist/esm/i18n/locales/locales/es-ES.json +13 -1
  37. package/dist/esm/index.js +28 -18
  38. package/dist/esm/mixins/OxyServices.accounts.js +6 -13
  39. package/dist/esm/mixins/OxyServices.auth.js +66 -201
  40. package/dist/esm/mixins/OxyServices.authorizedApps.js +35 -0
  41. package/dist/esm/mixins/OxyServices.deviceBoot.js +116 -0
  42. package/dist/esm/mixins/index.js +13 -17
  43. package/dist/esm/session/SessionClient.js +44 -2
  44. package/dist/esm/session/authStateStore.js +278 -0
  45. package/dist/esm/session/createSessionClient.js +8 -2
  46. package/dist/esm/session/refresh.js +257 -0
  47. package/dist/esm/utils/accountUtils.js +0 -53
  48. package/dist/esm/utils/authWebUrl.js +6 -14
  49. package/dist/esm/utils/fapiAutoDetect.js +16 -63
  50. package/dist/esm/utils/platform.js +18 -0
  51. package/dist/esm/utils/ssoBounce.js +14 -324
  52. package/dist/esm/utils/validationUtils.js +56 -0
  53. package/dist/types/.tsbuildinfo +1 -1
  54. package/dist/types/HttpService.d.ts +13 -0
  55. package/dist/types/OxyServices.base.d.ts +0 -52
  56. package/dist/types/OxyServices.d.ts +0 -25
  57. package/dist/types/boot/coldBootV2.d.ts +76 -0
  58. package/dist/types/boot/deviceBootReturn.d.ts +83 -0
  59. package/dist/types/crypto/keyManager.d.ts +21 -0
  60. package/dist/types/index.d.ts +16 -21
  61. package/dist/types/mixins/OxyServices.accounts.d.ts +0 -2
  62. package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
  63. package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
  64. package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
  65. package/dist/types/mixins/OxyServices.auth.d.ts +35 -77
  66. package/dist/types/mixins/{OxyServices.redirect.d.ts → OxyServices.authorizedApps.d.ts} +35 -33
  67. package/dist/types/mixins/OxyServices.civic.d.ts +0 -2
  68. package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
  69. package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
  70. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +110 -0
  71. package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
  72. package/dist/types/mixins/OxyServices.features.d.ts +0 -2
  73. package/dist/types/mixins/OxyServices.identity.d.ts +0 -2
  74. package/dist/types/mixins/OxyServices.language.d.ts +0 -2
  75. package/dist/types/mixins/OxyServices.links.d.ts +0 -2
  76. package/dist/types/mixins/OxyServices.location.d.ts +0 -2
  77. package/dist/types/mixins/OxyServices.nodes.d.ts +0 -2
  78. package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
  79. package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
  80. package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
  81. package/dist/types/mixins/OxyServices.security.d.ts +0 -2
  82. package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
  83. package/dist/types/mixins/OxyServices.user.d.ts +0 -2
  84. package/dist/types/mixins/OxyServices.utility.d.ts +0 -2
  85. package/dist/types/mixins/index.d.ts +6 -9
  86. package/dist/types/models/interfaces.d.ts +0 -67
  87. package/dist/types/session/SessionClient.d.ts +38 -1
  88. package/dist/types/session/authStateStore.d.ts +119 -0
  89. package/dist/types/session/createSessionClient.d.ts +8 -1
  90. package/dist/types/session/refresh.d.ts +93 -0
  91. package/dist/types/utils/accountUtils.d.ts +0 -14
  92. package/dist/types/utils/authWebUrl.d.ts +6 -12
  93. package/dist/types/utils/fapiAutoDetect.d.ts +15 -38
  94. package/dist/types/utils/platform.d.ts +14 -0
  95. package/dist/types/utils/ssoBounce.d.ts +14 -262
  96. package/dist/types/utils/validationUtils.d.ts +15 -0
  97. package/package.json +2 -2
  98. package/src/HttpService.ts +19 -3
  99. package/src/OxyServices.base.ts +7 -112
  100. package/src/OxyServices.ts +0 -38
  101. package/src/boot/__tests__/coldBootV2.test.ts +317 -0
  102. package/src/boot/__tests__/deviceBootReturn.test.ts +158 -0
  103. package/src/boot/coldBootV2.ts +426 -0
  104. package/src/boot/deviceBootReturn.ts +195 -0
  105. package/src/crypto/__tests__/sharedDeviceToken.test.ts +24 -0
  106. package/src/crypto/keyManager.ts +101 -0
  107. package/src/i18n/locales/en-US.json +13 -1
  108. package/src/i18n/locales/es-ES.json +13 -1
  109. package/src/index.ts +78 -64
  110. package/src/mixins/OxyServices.accounts.ts +6 -13
  111. package/src/mixins/OxyServices.auth.ts +78 -253
  112. package/src/mixins/OxyServices.authorizedApps.ts +75 -0
  113. package/src/mixins/OxyServices.deviceBoot.ts +146 -0
  114. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +107 -0
  115. package/src/mixins/__tests__/accounts.test.ts +17 -44
  116. package/src/mixins/__tests__/authorizedApps.test.ts +63 -0
  117. package/src/mixins/__tests__/passwordSignIn.test.ts +91 -0
  118. package/src/mixins/index.ts +16 -22
  119. package/src/models/interfaces.ts +0 -79
  120. package/src/session/SessionClient.ts +69 -3
  121. package/src/session/__tests__/SessionClient.additive.test.ts +92 -0
  122. package/src/session/__tests__/SessionClient.rest.test.ts +25 -0
  123. package/src/session/__tests__/SessionClient.socketFactory.test.ts +79 -0
  124. package/src/session/__tests__/SessionClient.state.test.ts +18 -5
  125. package/src/session/__tests__/authStateStore.test.ts +209 -0
  126. package/src/session/__tests__/refresh.test.ts +256 -0
  127. package/src/session/authStateStore.ts +335 -0
  128. package/src/session/createSessionClient.ts +9 -1
  129. package/src/session/refresh.ts +334 -0
  130. package/src/utils/__tests__/authWebUrl.test.ts +5 -29
  131. package/src/utils/__tests__/fapiAutoDetect.test.ts +5 -126
  132. package/src/utils/__tests__/validationUtils.test.ts +30 -0
  133. package/src/utils/accountUtils.ts +0 -62
  134. package/src/utils/authWebUrl.ts +6 -15
  135. package/src/utils/fapiAutoDetect.ts +16 -60
  136. package/src/utils/platform.ts +21 -0
  137. package/src/utils/ssoBounce.ts +14 -371
  138. package/src/utils/validationUtils.ts +62 -0
  139. package/dist/cjs/AuthManager.js +0 -1110
  140. package/dist/cjs/AuthManagerTypes.js +0 -13
  141. package/dist/cjs/CrossDomainAuth.js +0 -206
  142. package/dist/cjs/mixins/OxyServices.fedcm.js +0 -823
  143. package/dist/cjs/mixins/OxyServices.redirect.js +0 -95
  144. package/dist/cjs/mixins/OxyServices.silent.js +0 -204
  145. package/dist/cjs/mixins/OxyServices.sso.js +0 -208
  146. package/dist/cjs/utils/ssoEstablish.js +0 -110
  147. package/dist/cjs/utils/ssoReturn.js +0 -267
  148. package/dist/esm/AuthManager.js +0 -1105
  149. package/dist/esm/AuthManagerTypes.js +0 -12
  150. package/dist/esm/CrossDomainAuth.js +0 -201
  151. package/dist/esm/mixins/OxyServices.fedcm.js +0 -821
  152. package/dist/esm/mixins/OxyServices.redirect.js +0 -92
  153. package/dist/esm/mixins/OxyServices.silent.js +0 -202
  154. package/dist/esm/mixins/OxyServices.sso.js +0 -204
  155. package/dist/esm/utils/ssoEstablish.js +0 -107
  156. package/dist/esm/utils/ssoReturn.js +0 -263
  157. package/dist/types/AuthManager.d.ts +0 -380
  158. package/dist/types/AuthManagerTypes.d.ts +0 -81
  159. package/dist/types/CrossDomainAuth.d.ts +0 -164
  160. package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -331
  161. package/dist/types/mixins/OxyServices.silent.d.ts +0 -132
  162. package/dist/types/mixins/OxyServices.sso.d.ts +0 -138
  163. package/dist/types/utils/ssoEstablish.d.ts +0 -85
  164. package/dist/types/utils/ssoReturn.d.ts +0 -146
  165. package/src/AuthManager.ts +0 -1269
  166. package/src/AuthManagerTypes.ts +0 -86
  167. package/src/CrossDomainAuth.ts +0 -243
  168. package/src/__tests__/authManager.cookiePath.test.ts +0 -390
  169. package/src/__tests__/authManager.security.test.ts +0 -377
  170. package/src/__tests__/crossDomainAuth.test.ts +0 -116
  171. package/src/__tests__/establishDeviceRefreshSlot.test.ts +0 -221
  172. package/src/mixins/OxyServices.fedcm.ts +0 -1026
  173. package/src/mixins/OxyServices.redirect.ts +0 -122
  174. package/src/mixins/OxyServices.silent.ts +0 -272
  175. package/src/mixins/OxyServices.sso.ts +0 -261
  176. package/src/mixins/__tests__/constructorAuthWebUrl.test.ts +0 -85
  177. package/src/mixins/__tests__/fedcm.test.ts +0 -667
  178. package/src/mixins/__tests__/sessionBaseUrl.test.ts +0 -61
  179. package/src/mixins/__tests__/silent.test.ts +0 -102
  180. package/src/mixins/__tests__/sso.test.ts +0 -228
  181. package/src/utils/__tests__/consumeSsoReturn.test.ts +0 -816
  182. package/src/utils/__tests__/ssoBounce.test.ts +0 -219
  183. package/src/utils/__tests__/ssoEstablish.test.ts +0 -204
  184. package/src/utils/__tests__/ssoReturn.test.ts +0 -251
  185. package/src/utils/ssoEstablish.ts +0 -174
  186. package/src/utils/ssoReturn.ts +0 -372
@@ -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
- });