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