@oxyhq/core 3.4.0 → 3.4.2
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/AuthManager.js +91 -319
- package/dist/cjs/CrossDomainAuth.js +19 -106
- package/dist/cjs/HttpService.js +49 -73
- package/dist/cjs/OxyServices.base.js +2 -2
- package/dist/cjs/i18n/index.js +7 -1
- package/dist/cjs/i18n/locales/ar-SA.json +18 -2
- package/dist/cjs/i18n/locales/ca-ES.json +18 -2
- package/dist/cjs/i18n/locales/de-DE.json +18 -2
- package/dist/cjs/i18n/locales/en-US.json +16 -2
- package/dist/cjs/i18n/locales/es-ES.json +16 -2
- package/dist/cjs/i18n/locales/fr-FR.json +18 -2
- package/dist/cjs/i18n/locales/it-IT.json +18 -2
- package/dist/cjs/i18n/locales/ja-JP.json +18 -2
- package/dist/cjs/i18n/locales/ko-KR.json +18 -2
- package/dist/cjs/i18n/locales/locales/ar-SA.json +18 -2
- package/dist/cjs/i18n/locales/locales/ca-ES.json +18 -2
- package/dist/cjs/i18n/locales/locales/de-DE.json +18 -2
- package/dist/cjs/i18n/locales/locales/en-US.json +17 -3
- package/dist/cjs/i18n/locales/locales/es-ES.json +16 -2
- package/dist/cjs/i18n/locales/locales/fr-FR.json +18 -2
- package/dist/cjs/i18n/locales/locales/it-IT.json +18 -2
- package/dist/cjs/i18n/locales/locales/ja-JP.json +18 -2
- package/dist/cjs/i18n/locales/locales/ko-KR.json +18 -2
- package/dist/cjs/i18n/locales/locales/pt-PT.json +18 -2
- package/dist/cjs/i18n/locales/locales/zh-CN.json +18 -2
- package/dist/cjs/i18n/locales/pt-PT.json +18 -2
- package/dist/cjs/i18n/locales/zh-CN.json +18 -2
- package/dist/cjs/mixins/OxyServices.auth.js +20 -63
- package/dist/cjs/mixins/OxyServices.fedcm.js +10 -12
- package/dist/cjs/mixins/OxyServices.popup.js +50 -299
- package/dist/cjs/mixins/OxyServices.redirect.js +84 -348
- package/dist/cjs/mixins/OxyServices.silent.js +204 -0
- package/dist/cjs/mixins/OxyServices.sso.js +4 -5
- package/dist/cjs/mixins/OxyServices.utility.js +6 -15
- package/dist/cjs/mixins/index.js +5 -6
- package/dist/cjs/server/index.js +21 -0
- package/dist/cjs/server/rateLimit.js +77 -0
- package/dist/cjs/shared/utils/debugUtils.js +1 -1
- package/dist/cjs/utils/accountUtils.js +4 -4
- package/dist/cjs/utils/authHelpers.js +21 -15
- package/dist/cjs/utils/coldBoot.js +3 -3
- package/dist/cjs/utils/fapiAutoDetect.js +1 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/AuthManager.js +91 -319
- package/dist/esm/CrossDomainAuth.js +19 -106
- package/dist/esm/HttpService.js +49 -73
- package/dist/esm/OxyServices.base.js +2 -2
- package/dist/esm/i18n/index.js +7 -1
- package/dist/esm/i18n/locales/ar-SA.json +18 -2
- package/dist/esm/i18n/locales/ca-ES.json +18 -2
- package/dist/esm/i18n/locales/de-DE.json +18 -2
- package/dist/esm/i18n/locales/en-US.json +16 -2
- package/dist/esm/i18n/locales/es-ES.json +16 -2
- package/dist/esm/i18n/locales/fr-FR.json +18 -2
- package/dist/esm/i18n/locales/it-IT.json +18 -2
- package/dist/esm/i18n/locales/ja-JP.json +18 -2
- package/dist/esm/i18n/locales/ko-KR.json +18 -2
- package/dist/esm/i18n/locales/locales/ar-SA.json +18 -2
- package/dist/esm/i18n/locales/locales/ca-ES.json +18 -2
- package/dist/esm/i18n/locales/locales/de-DE.json +18 -2
- package/dist/esm/i18n/locales/locales/en-US.json +17 -3
- package/dist/esm/i18n/locales/locales/es-ES.json +16 -2
- package/dist/esm/i18n/locales/locales/fr-FR.json +18 -2
- package/dist/esm/i18n/locales/locales/it-IT.json +18 -2
- package/dist/esm/i18n/locales/locales/ja-JP.json +18 -2
- package/dist/esm/i18n/locales/locales/ko-KR.json +18 -2
- package/dist/esm/i18n/locales/locales/pt-PT.json +18 -2
- package/dist/esm/i18n/locales/locales/zh-CN.json +18 -2
- package/dist/esm/i18n/locales/pt-PT.json +18 -2
- package/dist/esm/i18n/locales/zh-CN.json +18 -2
- package/dist/esm/mixins/OxyServices.auth.js +20 -63
- package/dist/esm/mixins/OxyServices.fedcm.js +10 -12
- package/dist/esm/mixins/OxyServices.popup.js +52 -301
- package/dist/esm/mixins/OxyServices.redirect.js +84 -349
- package/dist/esm/mixins/OxyServices.silent.js +202 -0
- package/dist/esm/mixins/OxyServices.sso.js +4 -5
- package/dist/esm/mixins/OxyServices.utility.js +6 -15
- package/dist/esm/mixins/index.js +5 -6
- package/dist/esm/server/index.js +17 -0
- package/dist/esm/server/rateLimit.js +71 -0
- package/dist/esm/shared/utils/debugUtils.js +1 -1
- package/dist/esm/utils/accountUtils.js +4 -4
- package/dist/esm/utils/authHelpers.js +21 -15
- package/dist/esm/utils/coldBoot.js +3 -3
- package/dist/esm/utils/fapiAutoDetect.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/AuthManager.d.ts +26 -53
- package/dist/types/AuthManagerTypes.d.ts +5 -9
- package/dist/types/CrossDomainAuth.d.ts +13 -52
- package/dist/types/HttpService.d.ts +9 -8
- package/dist/types/OxyServices.base.d.ts +1 -1
- package/dist/types/OxyServices.d.ts +4 -10
- package/dist/types/index.d.ts +1 -1
- package/dist/types/mixins/OxyServices.analytics.d.ts +1 -1
- package/dist/types/mixins/OxyServices.appData.d.ts +1 -1
- package/dist/types/mixins/OxyServices.applications.d.ts +1 -1
- package/dist/types/mixins/OxyServices.assets.d.ts +1 -1
- package/dist/types/mixins/OxyServices.auth.d.ts +10 -31
- package/dist/types/mixins/OxyServices.contacts.d.ts +1 -1
- package/dist/types/mixins/OxyServices.devices.d.ts +1 -1
- package/dist/types/mixins/OxyServices.features.d.ts +1 -1
- package/dist/types/mixins/OxyServices.fedcm.d.ts +5 -5
- package/dist/types/mixins/OxyServices.language.d.ts +1 -1
- package/dist/types/mixins/OxyServices.location.d.ts +1 -1
- package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -1
- package/dist/types/mixins/OxyServices.payment.d.ts +1 -1
- package/dist/types/mixins/OxyServices.popup.d.ts +18 -120
- package/dist/types/mixins/OxyServices.privacy.d.ts +1 -1
- package/dist/types/mixins/OxyServices.redirect.d.ts +13 -174
- package/dist/types/mixins/OxyServices.reputation.d.ts +1 -1
- package/dist/types/mixins/OxyServices.security.d.ts +1 -1
- package/dist/types/mixins/OxyServices.silent.d.ts +131 -0
- package/dist/types/mixins/OxyServices.sso.d.ts +4 -5
- package/dist/types/mixins/OxyServices.topics.d.ts +1 -1
- package/dist/types/mixins/OxyServices.user.d.ts +1 -1
- package/dist/types/mixins/OxyServices.utility.d.ts +3 -8
- package/dist/types/mixins/OxyServices.workspaces.d.ts +1 -1
- package/dist/types/mixins/index.d.ts +3 -3
- package/dist/types/models/interfaces.d.ts +5 -16
- package/dist/types/models/session.d.ts +0 -2
- package/dist/types/server/index.d.ts +18 -0
- package/dist/types/server/rateLimit.d.ts +40 -0
- package/dist/types/shared/utils/debugUtils.d.ts +1 -1
- package/dist/types/utils/authHelpers.d.ts +4 -3
- package/dist/types/utils/coldBoot.d.ts +2 -2
- package/dist/types/utils/fapiAutoDetect.d.ts +1 -1
- package/package.json +25 -3
- package/src/AuthManager.ts +100 -370
- package/src/AuthManagerTypes.ts +5 -9
- package/src/CrossDomainAuth.ts +22 -129
- package/src/HttpService.ts +55 -73
- package/src/OxyServices.base.ts +2 -3
- package/src/OxyServices.ts +9 -11
- package/src/__tests__/authManager.cookiePath.test.ts +19 -17
- package/src/__tests__/authManager.security.test.ts +7 -3
- package/src/__tests__/crossDomainAuth.test.ts +26 -118
- package/src/i18n/index.ts +7 -1
- package/src/i18n/locales/ar-SA.json +18 -2
- package/src/i18n/locales/ca-ES.json +18 -2
- package/src/i18n/locales/de-DE.json +18 -2
- package/src/i18n/locales/en-US.json +17 -3
- package/src/i18n/locales/es-ES.json +16 -2
- package/src/i18n/locales/fr-FR.json +18 -2
- package/src/i18n/locales/it-IT.json +18 -2
- package/src/i18n/locales/ja-JP.json +18 -2
- package/src/i18n/locales/ko-KR.json +18 -2
- package/src/i18n/locales/pt-PT.json +18 -2
- package/src/i18n/locales/zh-CN.json +18 -2
- package/src/index.ts +1 -1
- package/src/mixins/OxyServices.auth.ts +23 -75
- package/src/mixins/OxyServices.fedcm.ts +10 -12
- package/src/mixins/OxyServices.redirect.ts +82 -371
- package/src/mixins/OxyServices.silent.ts +272 -0
- package/src/mixins/OxyServices.sso.ts +5 -6
- package/src/mixins/OxyServices.utility.ts +9 -22
- package/src/mixins/__tests__/appData.test.ts +1 -1
- package/src/mixins/__tests__/onTokensChanged.test.ts +1 -1
- package/src/mixins/__tests__/reputation.test.ts +1 -1
- package/src/mixins/__tests__/serviceAuth.test.ts +7 -5
- package/src/mixins/__tests__/silent.test.ts +102 -0
- package/src/mixins/__tests__/verifyChallenge.test.ts +9 -14
- package/src/mixins/index.ts +6 -8
- package/src/models/interfaces.ts +5 -16
- package/src/models/session.ts +1 -3
- package/src/server/index.ts +19 -0
- package/src/server/rateLimit.ts +170 -0
- package/src/shared/utils/debugUtils.ts +1 -1
- package/src/utils/accountUtils.ts +4 -4
- package/src/utils/authHelpers.ts +23 -15
- package/src/utils/coldBoot.ts +4 -4
- package/src/utils/fapiAutoDetect.ts +1 -1
- package/src/mixins/OxyServices.popup.ts +0 -631
- package/src/mixins/__tests__/popup.test.ts +0 -374
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Popup mixin regression tests.
|
|
3
|
-
*
|
|
4
|
-
* Locks in the §6c fix: cross-domain sign-in popups (auth.oxy.so) were being
|
|
5
|
-
* blocked by Chrome on consumer apps (mention.earth, homiio.com, alia.onl)
|
|
6
|
-
* because the caller chain awaited FedCM / silent SSO BEFORE
|
|
7
|
-
* `signInWithPopup` reached `window.open`. The transient user-activation
|
|
8
|
-
* had been consumed by the first `await`, so the popup-blocker killed the
|
|
9
|
-
* subsequent `window.open` call.
|
|
10
|
-
*
|
|
11
|
-
* The fix exposes two new affordances on the popup mixin:
|
|
12
|
-
* 1. `openBlankPopup(width?, height?)` — a public, synchronous helper that
|
|
13
|
-
* callers invoke from the raw user-gesture handler BEFORE any await, so
|
|
14
|
-
* the popup is opened while the activation is still live.
|
|
15
|
-
* 2. `signInWithPopup({ popup })` — accepts the pre-opened window handle
|
|
16
|
-
* and navigates IT to the auth URL instead of issuing a fresh
|
|
17
|
-
* `window.open` (which would now be blocked).
|
|
18
|
-
*
|
|
19
|
-
* Backward compat: callers that omit `popup` keep the legacy behaviour (the
|
|
20
|
-
* mixin opens its own popup via `openCenteredPopup`). The browser globals
|
|
21
|
-
* are stubbed so the platform-agnostic mixin can run under the node test
|
|
22
|
-
* env.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { OxyServices } from '../../OxyServices';
|
|
26
|
-
|
|
27
|
-
const ORIGIN = 'https://mention.earth';
|
|
28
|
-
|
|
29
|
-
interface MockPopup {
|
|
30
|
-
closed: boolean;
|
|
31
|
-
close: jest.Mock;
|
|
32
|
-
location: {
|
|
33
|
-
href: string;
|
|
34
|
-
replace: jest.Mock;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function createMockPopup(overrides: Partial<MockPopup> = {}): MockPopup {
|
|
39
|
-
return {
|
|
40
|
-
closed: false,
|
|
41
|
-
close: jest.fn(),
|
|
42
|
-
location: {
|
|
43
|
-
href: '',
|
|
44
|
-
replace: jest.fn(function (this: { href: string }, url: string) {
|
|
45
|
-
this.href = url;
|
|
46
|
-
}),
|
|
47
|
-
},
|
|
48
|
-
...overrides,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function installBrowserGlobals(options: {
|
|
53
|
-
windowOpen?: jest.Mock;
|
|
54
|
-
postMessageDispatcher?: { current: ((event: { origin: string; data: unknown }) => void) | null };
|
|
55
|
-
} = {}): void {
|
|
56
|
-
const store = new Map<string, string>();
|
|
57
|
-
const sessionStorageStub = {
|
|
58
|
-
getItem: (k: string) => (store.has(k) ? (store.get(k) as string) : null),
|
|
59
|
-
setItem: (k: string, v: string) => { store.set(k, v); },
|
|
60
|
-
removeItem: (k: string) => { store.delete(k); },
|
|
61
|
-
};
|
|
62
|
-
const messageHandlers: Array<(event: { origin: string; data: unknown }) => void> = [];
|
|
63
|
-
const win = {
|
|
64
|
-
location: { origin: ORIGIN, hostname: 'mention.earth' },
|
|
65
|
-
screenX: 0,
|
|
66
|
-
screenY: 0,
|
|
67
|
-
outerWidth: 1280,
|
|
68
|
-
outerHeight: 800,
|
|
69
|
-
sessionStorage: sessionStorageStub,
|
|
70
|
-
open: options.windowOpen ?? jest.fn(() => null),
|
|
71
|
-
addEventListener: (event: string, handler: (e: { origin: string; data: unknown }) => void) => {
|
|
72
|
-
if (event === 'message') {
|
|
73
|
-
messageHandlers.push(handler);
|
|
74
|
-
if (options.postMessageDispatcher) {
|
|
75
|
-
options.postMessageDispatcher.current = handler;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
removeEventListener: (event: string, handler: (e: { origin: string; data: unknown }) => void) => {
|
|
80
|
-
if (event === 'message') {
|
|
81
|
-
const idx = messageHandlers.indexOf(handler);
|
|
82
|
-
if (idx >= 0) messageHandlers.splice(idx, 1);
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
(globalThis as unknown as { window: unknown }).window = win;
|
|
87
|
-
(globalThis as unknown as { sessionStorage: unknown }).sessionStorage = sessionStorageStub;
|
|
88
|
-
// `crypto.randomUUID` already exists in node 20+ test env — leave it.
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function clearBrowserGlobals(): void {
|
|
92
|
-
for (const key of ['window', 'sessionStorage'] as const) {
|
|
93
|
-
delete (globalThis as Record<string, unknown>)[key];
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
describe('OxyServices popup mixin — pre-opened popup option', () => {
|
|
98
|
-
afterEach(() => {
|
|
99
|
-
clearBrowserGlobals();
|
|
100
|
-
jest.restoreAllMocks();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('navigates a pre-opened popup to the auth URL instead of opening a new one', async () => {
|
|
104
|
-
const windowOpen = jest.fn();
|
|
105
|
-
installBrowserGlobals({ windowOpen });
|
|
106
|
-
const popup = createMockPopup();
|
|
107
|
-
|
|
108
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
109
|
-
|
|
110
|
-
// Resolve `signInWithPopup` as soon as the popup is navigated — we only
|
|
111
|
-
// care about the open path, not the full postMessage round-trip.
|
|
112
|
-
let dispatchedAuthUrl: string | null = null;
|
|
113
|
-
popup.location.replace.mockImplementation(function (this: MockPopup['location'], url: string) {
|
|
114
|
-
this.href = url;
|
|
115
|
-
dispatchedAuthUrl = url;
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Fire the auth-success message immediately after navigation. We do this
|
|
119
|
-
// by intercepting `addEventListener` above.
|
|
120
|
-
const messagePromise = new Promise<void>((resolve) => {
|
|
121
|
-
// Patch addEventListener to capture the handler and dispatch a fake
|
|
122
|
-
// success message on the next microtask.
|
|
123
|
-
const origAdd = (globalThis as unknown as { window: { addEventListener: typeof window.addEventListener } }).window.addEventListener;
|
|
124
|
-
(globalThis as unknown as { window: { addEventListener: typeof window.addEventListener } }).window.addEventListener =
|
|
125
|
-
(event: string, handler: EventListenerOrEventListenerObject) => {
|
|
126
|
-
origAdd(event, handler);
|
|
127
|
-
if (event === 'message') {
|
|
128
|
-
queueMicrotask(() => {
|
|
129
|
-
// We need the state from the URL the popup was navigated to.
|
|
130
|
-
const url = new URL(dispatchedAuthUrl ?? '');
|
|
131
|
-
const state = url.searchParams.get('state') ?? '';
|
|
132
|
-
(handler as (e: { origin: string; data: unknown }) => void)({
|
|
133
|
-
origin: 'https://auth.oxy.so',
|
|
134
|
-
data: {
|
|
135
|
-
type: 'oxy_auth_response',
|
|
136
|
-
state,
|
|
137
|
-
session: {
|
|
138
|
-
sessionId: 'sess_pre_opened',
|
|
139
|
-
deviceId: 'dev_pre',
|
|
140
|
-
expiresAt: new Date(Date.now() + 60000).toISOString(),
|
|
141
|
-
accessToken: 'access_pre',
|
|
142
|
-
user: { id: 'user_pre', username: 'tester' },
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
resolve();
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const session = await oxy.signInWithPopup({ popup: popup as unknown as Window });
|
|
153
|
-
await messagePromise;
|
|
154
|
-
|
|
155
|
-
// The pre-opened popup was navigated — `window.open` was NEVER called.
|
|
156
|
-
expect(windowOpen).not.toHaveBeenCalled();
|
|
157
|
-
expect(popup.location.replace).toHaveBeenCalledTimes(1);
|
|
158
|
-
expect(popup.location.replace).toHaveBeenCalledWith(expect.stringContaining('https://auth.oxy.so/login'));
|
|
159
|
-
expect(session.sessionId).toBe('sess_pre_opened');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('throws a "window was closed" (cancelled) error — NOT "popup blocked" — when the pre-opened handle is already closed', async () => {
|
|
163
|
-
const windowOpen = jest.fn();
|
|
164
|
-
installBrowserGlobals({ windowOpen });
|
|
165
|
-
const popup = createMockPopup({ closed: true });
|
|
166
|
-
|
|
167
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
168
|
-
|
|
169
|
-
// The popup DID open (the blocker allowed it) and the user closed it.
|
|
170
|
-
// The error must communicate a cancel, never a blocker rejection —
|
|
171
|
-
// consumers map "blocked" to "please allow popups" UX guidance, which
|
|
172
|
-
// would be wrong here.
|
|
173
|
-
await expect(
|
|
174
|
-
oxy.signInWithPopup({ popup: popup as unknown as Window })
|
|
175
|
-
).rejects.toThrow(/Sign-in window was closed/);
|
|
176
|
-
await expect(
|
|
177
|
-
oxy.signInWithPopup({ popup: popup as unknown as Window })
|
|
178
|
-
).rejects.not.toThrow(/Popup blocked/);
|
|
179
|
-
|
|
180
|
-
// Did not attempt to open a fresh popup either.
|
|
181
|
-
expect(windowOpen).not.toHaveBeenCalled();
|
|
182
|
-
expect(popup.location.replace).not.toHaveBeenCalled();
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('falls back to assigning `location.href` when `location.replace` throws (sandboxed environments)', async () => {
|
|
186
|
-
const windowOpen = jest.fn();
|
|
187
|
-
installBrowserGlobals({ windowOpen });
|
|
188
|
-
|
|
189
|
-
// Some sandboxed / cross-origin-locked environments make `location.replace`
|
|
190
|
-
// throw. The mixin must recover with a plain `href` assignment so the
|
|
191
|
-
// popup still gets navigated. This is the only path that exercises the
|
|
192
|
-
// catch-and-log branch in OxyServices.popup.ts.
|
|
193
|
-
const popup: MockPopup = {
|
|
194
|
-
closed: false,
|
|
195
|
-
close: jest.fn(),
|
|
196
|
-
location: {
|
|
197
|
-
href: '',
|
|
198
|
-
replace: jest.fn(() => {
|
|
199
|
-
throw new Error('SecurityError: replace blocked by sandbox');
|
|
200
|
-
}),
|
|
201
|
-
},
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
205
|
-
|
|
206
|
-
// Drive `signInWithPopup` only until the navigation happens, then abort
|
|
207
|
-
// via `closed = true` so the poll loop's cancel path resolves the promise.
|
|
208
|
-
const promise = oxy.signInWithPopup({ popup: popup as unknown as Window });
|
|
209
|
-
// Let the synchronous popup-navigation path run.
|
|
210
|
-
await Promise.resolve();
|
|
211
|
-
popup.closed = true;
|
|
212
|
-
|
|
213
|
-
await expect(promise).rejects.toThrow(/cancelled|timeout/i);
|
|
214
|
-
|
|
215
|
-
// `replace` was attempted (and threw); the fallback wrote to `href`.
|
|
216
|
-
expect(popup.location.replace).toHaveBeenCalledTimes(1);
|
|
217
|
-
expect(popup.location.href).toMatch(/^https:\/\/auth\.oxy\.so\/login/);
|
|
218
|
-
// No fresh popup was opened.
|
|
219
|
-
expect(windowOpen).not.toHaveBeenCalled();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('falls back to opening its own popup when `popup` option is omitted (classic behaviour)', async () => {
|
|
223
|
-
const fallbackPopup = createMockPopup();
|
|
224
|
-
const windowOpen = jest.fn(() => fallbackPopup);
|
|
225
|
-
installBrowserGlobals({ windowOpen });
|
|
226
|
-
|
|
227
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
228
|
-
|
|
229
|
-
// Don't actually wait for the full flow; intercept after `window.open`
|
|
230
|
-
// is called and abort.
|
|
231
|
-
const promise = oxy.signInWithPopup();
|
|
232
|
-
// Allow the synchronous `window.open` path to run.
|
|
233
|
-
await Promise.resolve();
|
|
234
|
-
fallbackPopup.closed = true; // trigger "Authentication cancelled by user"
|
|
235
|
-
|
|
236
|
-
await expect(promise).rejects.toThrow(/cancelled|timeout/i);
|
|
237
|
-
|
|
238
|
-
expect(windowOpen).toHaveBeenCalledTimes(1);
|
|
239
|
-
// The first arg is the auth URL (not 'about:blank').
|
|
240
|
-
expect((windowOpen.mock.calls[0] as unknown[])[0]).toMatch(/^https:\/\/auth\.oxy\.so\/login/);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('throws "popup blocked" when the legacy path is used and `window.open` returns null', async () => {
|
|
244
|
-
const windowOpen = jest.fn(() => null);
|
|
245
|
-
installBrowserGlobals({ windowOpen });
|
|
246
|
-
|
|
247
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
248
|
-
|
|
249
|
-
await expect(oxy.signInWithPopup()).rejects.toThrow(/Popup blocked/);
|
|
250
|
-
expect(windowOpen).toHaveBeenCalledTimes(1);
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
describe('OxyServices popup mixin — openBlankPopup helper', () => {
|
|
255
|
-
afterEach(() => {
|
|
256
|
-
clearBrowserGlobals();
|
|
257
|
-
jest.restoreAllMocks();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('opens about:blank synchronously and returns the window handle', () => {
|
|
261
|
-
const fakePopup = createMockPopup();
|
|
262
|
-
const windowOpen = jest.fn(() => fakePopup);
|
|
263
|
-
installBrowserGlobals({ windowOpen });
|
|
264
|
-
|
|
265
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
266
|
-
const popup = oxy.openBlankPopup();
|
|
267
|
-
|
|
268
|
-
expect(windowOpen).toHaveBeenCalledTimes(1);
|
|
269
|
-
const args = windowOpen.mock.calls[0] as unknown[];
|
|
270
|
-
expect(args[0]).toBe('about:blank');
|
|
271
|
-
expect(args[1]).toBe('Oxy Sign In');
|
|
272
|
-
// Features string should include the default width/height.
|
|
273
|
-
expect(typeof args[2]).toBe('string');
|
|
274
|
-
expect(args[2] as string).toContain('width=500');
|
|
275
|
-
expect(args[2] as string).toContain('height=700');
|
|
276
|
-
expect(popup).toBe(fakePopup);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('returns null when the browser blocks the popup', () => {
|
|
280
|
-
const windowOpen = jest.fn(() => null);
|
|
281
|
-
installBrowserGlobals({ windowOpen });
|
|
282
|
-
|
|
283
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
284
|
-
const popup = oxy.openBlankPopup();
|
|
285
|
-
|
|
286
|
-
expect(popup).toBeNull();
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('honors caller-supplied dimensions', () => {
|
|
290
|
-
const fakePopup = createMockPopup();
|
|
291
|
-
const windowOpen = jest.fn(() => fakePopup);
|
|
292
|
-
installBrowserGlobals({ windowOpen });
|
|
293
|
-
|
|
294
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
295
|
-
oxy.openBlankPopup(640, 480);
|
|
296
|
-
|
|
297
|
-
const args = windowOpen.mock.calls[0] as unknown[];
|
|
298
|
-
expect(args[2] as string).toContain('width=640');
|
|
299
|
-
expect(args[2] as string).toContain('height=480');
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('returns null in non-browser environments', () => {
|
|
303
|
-
// No browser globals installed.
|
|
304
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
305
|
-
expect(oxy.openBlankPopup()).toBeNull();
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* `waitForIframeAuth` fail-fast regression tests.
|
|
311
|
-
*
|
|
312
|
-
* The cross-domain durable-restore iframe (`/auth/silent` at the per-apex host)
|
|
313
|
-
* posts a message on success. On a FAILED load — host unreachable, blocked by
|
|
314
|
-
* CSP `frame-ancestors`/`X-Frame-Options`, or a dropped network — it never
|
|
315
|
-
* posts, so without an `onerror`/`onabort` handler the silent restore would
|
|
316
|
-
* block for the FULL timeout (dead latency in the cold-boot critical path). The
|
|
317
|
-
* handler must resolve `null` immediately on a load failure, well before the
|
|
318
|
-
* timeout fires.
|
|
319
|
-
*/
|
|
320
|
-
interface FakeIframe {
|
|
321
|
-
onerror: ((this: unknown, ...args: unknown[]) => unknown) | null;
|
|
322
|
-
onabort: ((this: unknown, ...args: unknown[]) => unknown) | null;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
describe('OxyServices waitForIframeAuth fail-fast on iframe load error', () => {
|
|
326
|
-
afterEach(() => {
|
|
327
|
-
clearBrowserGlobals();
|
|
328
|
-
jest.restoreAllMocks();
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it('resolves null immediately when the iframe fires onerror (does not wait for the timeout)', async () => {
|
|
332
|
-
installBrowserGlobals();
|
|
333
|
-
|
|
334
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
335
|
-
const iframe: FakeIframe = { onerror: null, onabort: null };
|
|
336
|
-
|
|
337
|
-
// A long timeout proves the resolution comes from `onerror`, not the timer.
|
|
338
|
-
const LONG_TIMEOUT = 100000;
|
|
339
|
-
const settled = oxy.waitForIframeAuth(
|
|
340
|
-
iframe as unknown as HTMLIFrameElement,
|
|
341
|
-
LONG_TIMEOUT,
|
|
342
|
-
'https://auth.mention.earth',
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
// The handler is installed synchronously; fire it on the next tick.
|
|
346
|
-
await Promise.resolve();
|
|
347
|
-
expect(typeof iframe.onerror).toBe('function');
|
|
348
|
-
iframe.onerror?.call(iframe);
|
|
349
|
-
|
|
350
|
-
await expect(settled).resolves.toBeNull();
|
|
351
|
-
// Cleanup detaches the handlers so a late event cannot double-resolve.
|
|
352
|
-
expect(iframe.onerror).toBeNull();
|
|
353
|
-
expect(iframe.onabort).toBeNull();
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it('resolves null immediately when the iframe fires onabort', async () => {
|
|
357
|
-
installBrowserGlobals();
|
|
358
|
-
|
|
359
|
-
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
360
|
-
const iframe: FakeIframe = { onerror: null, onabort: null };
|
|
361
|
-
|
|
362
|
-
const settled = oxy.waitForIframeAuth(
|
|
363
|
-
iframe as unknown as HTMLIFrameElement,
|
|
364
|
-
100000,
|
|
365
|
-
'https://auth.mention.earth',
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
await Promise.resolve();
|
|
369
|
-
expect(typeof iframe.onabort).toBe('function');
|
|
370
|
-
iframe.onabort?.call(iframe);
|
|
371
|
-
|
|
372
|
-
await expect(settled).resolves.toBeNull();
|
|
373
|
-
});
|
|
374
|
-
});
|