@oxyhq/core 1.11.9 → 1.11.10
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 +158 -1
- package/dist/cjs/crypto/keyManager.js +4 -6
- package/dist/cjs/crypto/polyfill.js +56 -12
- package/dist/cjs/crypto/signatureService.js +7 -4
- package/dist/cjs/mixins/OxyServices.fedcm.js +9 -4
- package/dist/cjs/mixins/OxyServices.popup.js +9 -5
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/AuthManager.js +158 -1
- package/dist/esm/crypto/keyManager.js +4 -6
- package/dist/esm/crypto/polyfill.js +23 -12
- package/dist/esm/crypto/signatureService.js +7 -4
- package/dist/esm/mixins/OxyServices.fedcm.js +9 -4
- package/dist/esm/mixins/OxyServices.popup.js +9 -5
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/AuthManager.d.ts +21 -0
- package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -0
- package/dist/types/mixins/OxyServices.popup.d.ts +2 -0
- package/package.json +1 -1
- package/src/AuthManager.ts +186 -4
- package/src/crypto/keyManager.ts +4 -6
- package/src/crypto/polyfill.ts +23 -12
- package/src/crypto/signatureService.ts +7 -4
- package/src/mixins/OxyServices.fedcm.ts +11 -4
- package/src/mixins/OxyServices.popup.ts +11 -5
package/dist/esm/AuthManager.js
CHANGED
|
@@ -102,17 +102,106 @@ export class AuthManager {
|
|
|
102
102
|
this.currentUser = null;
|
|
103
103
|
this.refreshTimer = null;
|
|
104
104
|
this.refreshPromise = null;
|
|
105
|
+
/** Tracks the access token this instance last knew about, for cross-tab adoption. */
|
|
106
|
+
this._lastKnownAccessToken = null;
|
|
107
|
+
/** BroadcastChannel for coordinating token refreshes across browser tabs. */
|
|
108
|
+
this._broadcastChannel = null;
|
|
109
|
+
/** Set to true when another tab broadcasts a successful refresh, so this tab can skip its own. */
|
|
110
|
+
this._otherTabRefreshed = false;
|
|
105
111
|
this.oxyServices = oxyServices;
|
|
112
|
+
const crossTabSync = config.crossTabSync ?? (typeof BroadcastChannel !== 'undefined');
|
|
106
113
|
this.config = {
|
|
107
114
|
storage: config.storage ?? this.getDefaultStorage(),
|
|
108
115
|
autoRefresh: config.autoRefresh ?? true,
|
|
109
116
|
refreshBuffer: config.refreshBuffer ?? 5 * 60 * 1000, // 5 minutes
|
|
117
|
+
crossTabSync,
|
|
110
118
|
};
|
|
111
119
|
this.storage = this.config.storage;
|
|
112
120
|
// Persist tokens to storage when HttpService refreshes them automatically
|
|
113
121
|
this.oxyServices.httpService.onTokenRefreshed = (accessToken) => {
|
|
122
|
+
this._lastKnownAccessToken = accessToken;
|
|
114
123
|
this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
|
|
115
124
|
};
|
|
125
|
+
// Setup cross-tab coordination in browser environments
|
|
126
|
+
if (this.config.crossTabSync) {
|
|
127
|
+
this._initBroadcastChannel();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Initialize BroadcastChannel for cross-tab token refresh coordination.
|
|
132
|
+
* Only called in browser environments where BroadcastChannel is available.
|
|
133
|
+
*/
|
|
134
|
+
_initBroadcastChannel() {
|
|
135
|
+
if (typeof BroadcastChannel === 'undefined')
|
|
136
|
+
return;
|
|
137
|
+
try {
|
|
138
|
+
this._broadcastChannel = new BroadcastChannel('oxy_auth_sync');
|
|
139
|
+
this._broadcastChannel.onmessage = (event) => {
|
|
140
|
+
this._handleCrossTabMessage(event.data);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// BroadcastChannel not supported or blocked (e.g., opaque origins)
|
|
145
|
+
this._broadcastChannel = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Handle messages from other tabs about token refresh activity.
|
|
150
|
+
*/
|
|
151
|
+
async _handleCrossTabMessage(message) {
|
|
152
|
+
if (!message || !message.type)
|
|
153
|
+
return;
|
|
154
|
+
switch (message.type) {
|
|
155
|
+
case 'tokens_refreshed': {
|
|
156
|
+
// Another tab successfully refreshed. Signal to cancel our pending refresh.
|
|
157
|
+
this._otherTabRefreshed = true;
|
|
158
|
+
// Adopt the new tokens from shared storage
|
|
159
|
+
const newToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
160
|
+
if (newToken && newToken !== this._lastKnownAccessToken) {
|
|
161
|
+
this._lastKnownAccessToken = newToken;
|
|
162
|
+
this.oxyServices.httpService.setTokens(newToken);
|
|
163
|
+
// Re-read session for updated expiry and schedule next refresh
|
|
164
|
+
const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
|
165
|
+
if (sessionJson) {
|
|
166
|
+
try {
|
|
167
|
+
const session = JSON.parse(sessionJson);
|
|
168
|
+
if (session.expiresAt && this.config.autoRefresh) {
|
|
169
|
+
this.setupTokenRefresh(session.expiresAt);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Ignore parse errors
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case 'signed_out': {
|
|
180
|
+
// Another tab signed out. Clear our local state to stay consistent.
|
|
181
|
+
if (this.refreshTimer) {
|
|
182
|
+
clearTimeout(this.refreshTimer);
|
|
183
|
+
this.refreshTimer = null;
|
|
184
|
+
}
|
|
185
|
+
this.refreshPromise = null;
|
|
186
|
+
this._lastKnownAccessToken = null;
|
|
187
|
+
this.oxyServices.httpService.setTokens('');
|
|
188
|
+
this.currentUser = null;
|
|
189
|
+
this.notifyListeners();
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
// 'refresh_starting' is informational; we don't need to act on it currently
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Broadcast a message to other tabs.
|
|
197
|
+
*/
|
|
198
|
+
_broadcast(message) {
|
|
199
|
+
try {
|
|
200
|
+
this._broadcastChannel?.postMessage(message);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Channel closed or unavailable
|
|
204
|
+
}
|
|
116
205
|
}
|
|
117
206
|
/**
|
|
118
207
|
* Get default storage based on environment.
|
|
@@ -159,6 +248,7 @@ export class AuthManager {
|
|
|
159
248
|
async handleAuthSuccess(session, method = 'credentials') {
|
|
160
249
|
// Store tokens
|
|
161
250
|
if (session.accessToken) {
|
|
251
|
+
this._lastKnownAccessToken = session.accessToken;
|
|
162
252
|
await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, session.accessToken);
|
|
163
253
|
this.oxyServices.httpService.setTokens(session.accessToken);
|
|
164
254
|
}
|
|
@@ -223,6 +313,8 @@ export class AuthManager {
|
|
|
223
313
|
}
|
|
224
314
|
}
|
|
225
315
|
async _doRefreshToken() {
|
|
316
|
+
// Reset the cross-tab flag before starting
|
|
317
|
+
this._otherTabRefreshed = false;
|
|
226
318
|
// Get session info to find sessionId for token refresh
|
|
227
319
|
const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
|
228
320
|
if (!sessionJson) {
|
|
@@ -239,8 +331,22 @@ export class AuthManager {
|
|
|
239
331
|
console.error('AuthManager: Failed to parse session from storage.', err);
|
|
240
332
|
return false;
|
|
241
333
|
}
|
|
334
|
+
// Record the token we know about before attempting refresh
|
|
335
|
+
const tokenBeforeRefresh = this._lastKnownAccessToken;
|
|
336
|
+
// Broadcast that we're starting a refresh (informational for other tabs)
|
|
337
|
+
this._broadcast({ type: 'refresh_starting', sessionId, timestamp: Date.now() });
|
|
242
338
|
try {
|
|
243
339
|
await retryAsync(async () => {
|
|
340
|
+
// Before each attempt, check if another tab already refreshed
|
|
341
|
+
if (this._otherTabRefreshed) {
|
|
342
|
+
const adoptedToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
343
|
+
if (adoptedToken && adoptedToken !== tokenBeforeRefresh) {
|
|
344
|
+
// Another tab succeeded. Adopt its tokens and short-circuit.
|
|
345
|
+
this._lastKnownAccessToken = adoptedToken;
|
|
346
|
+
this.oxyServices.httpService.setTokens(adoptedToken);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
244
350
|
const httpService = this.oxyServices.httpService;
|
|
245
351
|
// Use session-based token endpoint which handles auto-refresh server-side
|
|
246
352
|
const response = await httpService.request({
|
|
@@ -253,6 +359,7 @@ export class AuthManager {
|
|
|
253
359
|
throw new Error('No access token in refresh response');
|
|
254
360
|
}
|
|
255
361
|
// Update access token in storage and HTTP client
|
|
362
|
+
this._lastKnownAccessToken = response.accessToken;
|
|
256
363
|
await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, response.accessToken);
|
|
257
364
|
this.oxyServices.httpService.setTokens(response.accessToken);
|
|
258
365
|
// Update session expiry and schedule next refresh
|
|
@@ -270,6 +377,8 @@ export class AuthManager {
|
|
|
270
377
|
this.setupTokenRefresh(response.expiresAt);
|
|
271
378
|
}
|
|
272
379
|
}
|
|
380
|
+
// Broadcast success so other tabs can adopt these tokens
|
|
381
|
+
this._broadcast({ type: 'tokens_refreshed', sessionId, timestamp: Date.now() });
|
|
273
382
|
}, 2, // 2 retries = 3 total attempts
|
|
274
383
|
1000, // 1s base delay with exponential backoff + jitter
|
|
275
384
|
(error) => {
|
|
@@ -282,7 +391,41 @@ export class AuthManager {
|
|
|
282
391
|
return true;
|
|
283
392
|
}
|
|
284
393
|
catch {
|
|
285
|
-
// All retry attempts exhausted,
|
|
394
|
+
// All retry attempts exhausted. Before clearing the session, check if
|
|
395
|
+
// another tab managed to refresh successfully while we were retrying.
|
|
396
|
+
// Since all tabs share the same storage (localStorage), a successful
|
|
397
|
+
// refresh from another tab will have written a different access token.
|
|
398
|
+
const currentStoredToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
399
|
+
if (currentStoredToken && currentStoredToken !== tokenBeforeRefresh) {
|
|
400
|
+
// Another tab refreshed successfully. Adopt its tokens instead of logging out.
|
|
401
|
+
this._lastKnownAccessToken = currentStoredToken;
|
|
402
|
+
this.oxyServices.httpService.setTokens(currentStoredToken);
|
|
403
|
+
// Restore user from storage in case it was updated
|
|
404
|
+
const userJson = await this.storage.getItem(STORAGE_KEYS.USER);
|
|
405
|
+
if (userJson) {
|
|
406
|
+
try {
|
|
407
|
+
this.currentUser = JSON.parse(userJson);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
// Ignore parse errors
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Re-read session expiry and schedule next refresh
|
|
414
|
+
const updatedSessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
|
415
|
+
if (updatedSessionJson) {
|
|
416
|
+
try {
|
|
417
|
+
const session = JSON.parse(updatedSessionJson);
|
|
418
|
+
if (session.expiresAt && this.config.autoRefresh) {
|
|
419
|
+
this.setupTokenRefresh(session.expiresAt);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// Ignore parse errors
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
// No other tab rescued us -- truly clear the session
|
|
286
429
|
await this.clearSession();
|
|
287
430
|
this.currentUser = null;
|
|
288
431
|
this.notifyListeners();
|
|
@@ -324,8 +467,11 @@ export class AuthManager {
|
|
|
324
467
|
}
|
|
325
468
|
// Clear HTTP client tokens
|
|
326
469
|
this.oxyServices.httpService.setTokens('');
|
|
470
|
+
this._lastKnownAccessToken = null;
|
|
327
471
|
// Clear storage
|
|
328
472
|
await this.clearSession();
|
|
473
|
+
// Notify other tabs so they also sign out
|
|
474
|
+
this._broadcast({ type: 'signed_out', timestamp: Date.now() });
|
|
329
475
|
// Update state and notify
|
|
330
476
|
this.currentUser = null;
|
|
331
477
|
this.notifyListeners();
|
|
@@ -400,6 +546,7 @@ export class AuthManager {
|
|
|
400
546
|
// Restore token to HTTP client
|
|
401
547
|
const token = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
402
548
|
if (token) {
|
|
549
|
+
this._lastKnownAccessToken = token;
|
|
403
550
|
this.oxyServices.httpService.setTokens(token);
|
|
404
551
|
}
|
|
405
552
|
// Check session expiry
|
|
@@ -440,6 +587,16 @@ export class AuthManager {
|
|
|
440
587
|
this.refreshTimer = null;
|
|
441
588
|
}
|
|
442
589
|
this.listeners.clear();
|
|
590
|
+
// Close BroadcastChannel
|
|
591
|
+
if (this._broadcastChannel) {
|
|
592
|
+
try {
|
|
593
|
+
this._broadcastChannel.close();
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// Ignore close errors
|
|
597
|
+
}
|
|
598
|
+
this._broadcastChannel = null;
|
|
599
|
+
}
|
|
443
600
|
}
|
|
444
601
|
}
|
|
445
602
|
/**
|
|
@@ -91,13 +91,11 @@ async function getSecureRandomBytes(length) {
|
|
|
91
91
|
return Crypto.getRandomBytes(length);
|
|
92
92
|
}
|
|
93
93
|
// In Node.js, use Node's crypto module
|
|
94
|
-
//
|
|
95
|
-
// This ensures the require is only evaluated in Node.js runtime, not during Metro bundling
|
|
94
|
+
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
96
95
|
try {
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
return new Uint8Array(crypto.randomBytes(length));
|
|
96
|
+
const cryptoModuleName = 'crypto';
|
|
97
|
+
const nodeCrypto = await import(cryptoModuleName);
|
|
98
|
+
return new Uint8Array(nodeCrypto.randomBytes(length));
|
|
101
99
|
}
|
|
102
100
|
catch (error) {
|
|
103
101
|
// Fallback to expo-crypto if Node crypto fails
|
|
@@ -27,27 +27,35 @@ if (!globalObject.Buffer) {
|
|
|
27
27
|
}
|
|
28
28
|
// Cache for expo-crypto module (lazy loaded only in React Native)
|
|
29
29
|
let expoCryptoModule = null;
|
|
30
|
-
let
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
let expoCryptoLoadPromise = null;
|
|
31
|
+
/**
|
|
32
|
+
* Eagerly start loading expo-crypto. The module is cached once resolved so
|
|
33
|
+
* the synchronous getRandomValues shim can read from it immediately.
|
|
34
|
+
* Uses dynamic import with variable indirection to prevent ESM bundlers
|
|
35
|
+
* (Vite, webpack) from statically resolving the specifier.
|
|
36
|
+
*/
|
|
37
|
+
function startExpoCryptoLoad() {
|
|
38
|
+
if (expoCryptoLoadPromise)
|
|
39
|
+
return;
|
|
40
|
+
expoCryptoLoadPromise = (async () => {
|
|
34
41
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (typeof require !== 'undefined') {
|
|
38
|
-
const moduleName = 'expo-crypto';
|
|
39
|
-
expoCryptoModule = require(moduleName);
|
|
40
|
-
}
|
|
42
|
+
const moduleName = 'expo-crypto';
|
|
43
|
+
expoCryptoModule = await import(moduleName);
|
|
41
44
|
}
|
|
42
45
|
catch {
|
|
43
46
|
// expo-crypto not available — expected in non-RN environments
|
|
44
47
|
}
|
|
45
|
-
}
|
|
48
|
+
})();
|
|
49
|
+
}
|
|
50
|
+
function getRandomBytesSync(byteCount) {
|
|
51
|
+
// Kick off loading if not already started (should have been started at module init)
|
|
52
|
+
startExpoCryptoLoad();
|
|
46
53
|
if (expoCryptoModule) {
|
|
47
54
|
return expoCryptoModule.getRandomBytes(byteCount);
|
|
48
55
|
}
|
|
49
56
|
throw new Error('No crypto.getRandomValues implementation available. ' +
|
|
50
|
-
'In React Native, install expo-crypto.'
|
|
57
|
+
'In React Native, install expo-crypto. ' +
|
|
58
|
+
'If expo-crypto is installed, ensure the polyfill module is imported early enough for the async load to complete.');
|
|
51
59
|
}
|
|
52
60
|
const cryptoPolyfill = {
|
|
53
61
|
getRandomValues(array) {
|
|
@@ -59,9 +67,12 @@ const cryptoPolyfill = {
|
|
|
59
67
|
};
|
|
60
68
|
// Only polyfill if crypto or crypto.getRandomValues is not available
|
|
61
69
|
if (typeof globalObject.crypto === 'undefined') {
|
|
70
|
+
// Start loading expo-crypto eagerly so it is ready by the time getRandomValues is called
|
|
71
|
+
startExpoCryptoLoad();
|
|
62
72
|
globalObject.crypto = cryptoPolyfill;
|
|
63
73
|
}
|
|
64
74
|
else if (typeof globalObject.crypto.getRandomValues !== 'function') {
|
|
75
|
+
startExpoCryptoLoad();
|
|
65
76
|
globalObject.crypto.getRandomValues = cryptoPolyfill.getRandomValues;
|
|
66
77
|
}
|
|
67
78
|
export { Buffer };
|
|
@@ -63,11 +63,11 @@ export class SignatureService {
|
|
|
63
63
|
.join('');
|
|
64
64
|
}
|
|
65
65
|
// In Node.js, use Node's crypto module
|
|
66
|
+
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
66
67
|
if (isNodeJS()) {
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const nodeCrypto = getCrypto();
|
|
69
|
+
const cryptoModuleName = 'crypto';
|
|
70
|
+
const nodeCrypto = await import(cryptoModuleName);
|
|
71
71
|
return nodeCrypto.randomBytes(32).toString('hex');
|
|
72
72
|
}
|
|
73
73
|
catch {
|
|
@@ -134,7 +134,10 @@ export class SignatureService {
|
|
|
134
134
|
// In React Native, use async verify instead
|
|
135
135
|
throw new Error('verifySync should only be used in Node.js. Use verify() in React Native.');
|
|
136
136
|
}
|
|
137
|
-
//
|
|
137
|
+
// Intentionally using Function constructor here: this method is synchronous by design
|
|
138
|
+
// (Node.js backend hot-path) so we cannot use `await import()`. The Function constructor
|
|
139
|
+
// prevents Metro/bundlers from statically resolving the require. This is acceptable because
|
|
140
|
+
// verifySync is gated by isNodeJS() and will never execute in browser/RN environments.
|
|
138
141
|
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
139
142
|
const getCrypto = new Function('return require("crypto")');
|
|
140
143
|
const crypto = getCrypto();
|
|
@@ -34,6 +34,11 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
34
34
|
constructor(...args) {
|
|
35
35
|
super(...args);
|
|
36
36
|
}
|
|
37
|
+
resolveFedcmConfigUrl() {
|
|
38
|
+
return this.config.authWebUrl
|
|
39
|
+
? `${this.config.authWebUrl}/fedcm.json`
|
|
40
|
+
: this.constructor.DEFAULT_CONFIG_URL;
|
|
41
|
+
}
|
|
37
42
|
/**
|
|
38
43
|
* Check if FedCM is supported in the current browser
|
|
39
44
|
*/
|
|
@@ -85,7 +90,7 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
85
90
|
// Request credential from browser's native identity flow
|
|
86
91
|
// mode: 'button' signals this is a user-gesture-initiated flow (Chrome 125+)
|
|
87
92
|
const credential = await this.requestIdentityCredential({
|
|
88
|
-
configURL:
|
|
93
|
+
configURL: this.resolveFedcmConfigUrl(),
|
|
89
94
|
clientId,
|
|
90
95
|
nonce,
|
|
91
96
|
context: options.context,
|
|
@@ -175,7 +180,7 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
175
180
|
const nonce = this.generateNonce();
|
|
176
181
|
debug.log('Silent SSO: Attempting silent mediation...', loginHint ? `(hint: ${loginHint})` : '');
|
|
177
182
|
credential = await this.requestIdentityCredential({
|
|
178
|
-
configURL:
|
|
183
|
+
configURL: this.resolveFedcmConfigUrl(),
|
|
179
184
|
clientId,
|
|
180
185
|
nonce,
|
|
181
186
|
loginHint,
|
|
@@ -389,7 +394,7 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
389
394
|
if ('IdentityCredential' in window && 'disconnect' in window.IdentityCredential) {
|
|
390
395
|
const clientId = this.getClientId();
|
|
391
396
|
await window.IdentityCredential.disconnect({
|
|
392
|
-
configURL:
|
|
397
|
+
configURL: this.resolveFedcmConfigUrl(),
|
|
393
398
|
clientId,
|
|
394
399
|
accountHint: accountHint || '*',
|
|
395
400
|
});
|
|
@@ -408,7 +413,7 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
408
413
|
getFedCMConfig() {
|
|
409
414
|
return {
|
|
410
415
|
enabled: this.isFedCMSupported(),
|
|
411
|
-
configURL:
|
|
416
|
+
configURL: this.resolveFedcmConfigUrl(),
|
|
412
417
|
clientId: this.getClientId(),
|
|
413
418
|
};
|
|
414
419
|
}
|
|
@@ -29,6 +29,10 @@ export function OxyServicesPopupAuthMixin(Base) {
|
|
|
29
29
|
constructor(...args) {
|
|
30
30
|
super(...args);
|
|
31
31
|
}
|
|
32
|
+
/** Resolve auth URL from config or static default (method, not getter — getters break in TS mixins) */
|
|
33
|
+
resolveAuthUrl() {
|
|
34
|
+
return this.config.authWebUrl || this.constructor.DEFAULT_AUTH_URL;
|
|
35
|
+
}
|
|
32
36
|
/**
|
|
33
37
|
* Sign in using popup window
|
|
34
38
|
*
|
|
@@ -71,7 +75,7 @@ export function OxyServicesPopupAuthMixin(Base) {
|
|
|
71
75
|
state,
|
|
72
76
|
nonce,
|
|
73
77
|
clientId: window.location.origin,
|
|
74
|
-
redirectUri: `${
|
|
78
|
+
redirectUri: `${this.resolveAuthUrl()}/auth/callback`,
|
|
75
79
|
});
|
|
76
80
|
const popup = this.openCenteredPopup(authUrl, 'Oxy Sign In', width, height);
|
|
77
81
|
if (!popup) {
|
|
@@ -159,7 +163,7 @@ export function OxyServicesPopupAuthMixin(Base) {
|
|
|
159
163
|
iframe.style.width = '0';
|
|
160
164
|
iframe.style.height = '0';
|
|
161
165
|
iframe.style.border = 'none';
|
|
162
|
-
const silentUrl = `${
|
|
166
|
+
const silentUrl = `${this.resolveAuthUrl()}/auth/silent?` + `client_id=${encodeURIComponent(clientId)}&` + `nonce=${nonce}`;
|
|
163
167
|
iframe.src = silentUrl;
|
|
164
168
|
document.body.appendChild(iframe);
|
|
165
169
|
try {
|
|
@@ -210,7 +214,7 @@ export function OxyServicesPopupAuthMixin(Base) {
|
|
|
210
214
|
reject(new OxyAuthenticationError('Authentication timeout'));
|
|
211
215
|
}, timeout);
|
|
212
216
|
const messageHandler = (event) => {
|
|
213
|
-
const authUrl =
|
|
217
|
+
const authUrl = this.resolveAuthUrl();
|
|
214
218
|
// Log all messages for debugging
|
|
215
219
|
if (event.data && typeof event.data === 'object' && event.data.type) {
|
|
216
220
|
debug.log('Message received:', {
|
|
@@ -282,7 +286,7 @@ export function OxyServicesPopupAuthMixin(Base) {
|
|
|
282
286
|
}, timeout);
|
|
283
287
|
const messageHandler = (event) => {
|
|
284
288
|
// Verify origin
|
|
285
|
-
if (event.origin !==
|
|
289
|
+
if (event.origin !== this.resolveAuthUrl()) {
|
|
286
290
|
return;
|
|
287
291
|
}
|
|
288
292
|
const { type, session } = event.data;
|
|
@@ -305,7 +309,7 @@ export function OxyServicesPopupAuthMixin(Base) {
|
|
|
305
309
|
* @private
|
|
306
310
|
*/
|
|
307
311
|
buildAuthUrl(params) {
|
|
308
|
-
const url = new URL(`${
|
|
312
|
+
const url = new URL(`${this.resolveAuthUrl()}/${params.mode}`);
|
|
309
313
|
url.searchParams.set('response_type', 'token');
|
|
310
314
|
url.searchParams.set('client_id', params.clientId);
|
|
311
315
|
url.searchParams.set('redirect_uri', params.redirectUri);
|