@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
|
@@ -29,8 +29,7 @@ const debug = createDebugLogger('SSO');
|
|
|
29
29
|
*
|
|
30
30
|
* Exposed as a module-level helper (in addition to the instance method below)
|
|
31
31
|
* so consumers that do not yet hold an `OxyServices` instance can still mint a
|
|
32
|
-
* bounce state.
|
|
33
|
-
* mixin uses for its CSRF state, with a `getRandomValues` fallback.
|
|
32
|
+
* bounce state. Uses `crypto.randomUUID` with a `getRandomValues` fallback.
|
|
34
33
|
*/
|
|
35
34
|
export function generateSsoState() {
|
|
36
35
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
@@ -51,8 +50,8 @@ export function OxyServicesSsoMixin(Base) {
|
|
|
51
50
|
/**
|
|
52
51
|
* Generate cryptographically secure state for the SSO bounce (CSRF
|
|
53
52
|
* protection). Delegates to the module-level {@link generateSsoState}
|
|
54
|
-
* helper, which uses
|
|
55
|
-
*
|
|
53
|
+
* helper, which uses `crypto.randomUUID` when available and falls back to
|
|
54
|
+
* `crypto.getRandomValues`.
|
|
56
55
|
*/
|
|
57
56
|
generateSsoState() {
|
|
58
57
|
return generateSsoState();
|
|
@@ -123,7 +122,7 @@ export function OxyServicesSsoMixin(Base) {
|
|
|
123
122
|
// Plant the access token exactly like exchangeIdTokenForSession does.
|
|
124
123
|
// The SSO exchange does not return a refresh token (the central store
|
|
125
124
|
// holds the refresh credential), so default it to an empty string.
|
|
126
|
-
this.httpService.setTokens(payload.accessToken
|
|
125
|
+
this.httpService.setTokens(payload.accessToken);
|
|
127
126
|
debug.log('SSO exchange complete:', { hasSession: !!payload.sessionId });
|
|
128
127
|
const session = {
|
|
129
128
|
sessionId: payload.sessionId,
|
|
@@ -259,20 +259,12 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
259
259
|
return true;
|
|
260
260
|
};
|
|
261
261
|
try {
|
|
262
|
-
// Extract token from Authorization header
|
|
262
|
+
// Extract token from Authorization header.
|
|
263
263
|
// Node/Express normalizes `Authorization` to a string; we guard
|
|
264
264
|
// against the (legal but unusual) string[] case anyway.
|
|
265
265
|
const rawAuthHeader = req.headers.authorization;
|
|
266
266
|
const authHeader = Array.isArray(rawAuthHeader) ? rawAuthHeader[0] : rawAuthHeader;
|
|
267
|
-
|
|
268
|
-
// Fallback to query params (useful for WebSocket upgrades)
|
|
269
|
-
if (!token) {
|
|
270
|
-
const q = req.query || {};
|
|
271
|
-
if (typeof q.token === 'string' && q.token)
|
|
272
|
-
token = q.token;
|
|
273
|
-
else if (typeof q.access_token === 'string' && q.access_token)
|
|
274
|
-
token = q.access_token;
|
|
275
|
-
}
|
|
267
|
+
const token = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
|
|
276
268
|
if (debug) {
|
|
277
269
|
logger.debug(`[oxy.auth] ${req.method} ${req.path} | token: ${!!token}`, {
|
|
278
270
|
component: 'auth',
|
|
@@ -420,13 +412,14 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
420
412
|
}
|
|
421
413
|
// Validate required service token fields
|
|
422
414
|
const appId = decoded.appId;
|
|
423
|
-
|
|
415
|
+
const credentialId = decoded.credentialId;
|
|
416
|
+
if (!appId || typeof credentialId !== 'string' || credentialId.length === 0) {
|
|
424
417
|
if (optional) {
|
|
425
418
|
req.userId = null;
|
|
426
419
|
req.user = null;
|
|
427
420
|
return next();
|
|
428
421
|
}
|
|
429
|
-
const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token: missing
|
|
422
|
+
const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token: missing required claims', code: 'INVALID_SERVICE_TOKEN', status: 401 };
|
|
430
423
|
if (onError)
|
|
431
424
|
return onError(error);
|
|
432
425
|
return res.status(401).json(error);
|
|
@@ -471,10 +464,8 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
471
464
|
req.serviceApp = {
|
|
472
465
|
appId,
|
|
473
466
|
appName: decoded.appName || 'unknown',
|
|
467
|
+
credentialId,
|
|
474
468
|
scopes: Array.isArray(decoded.scopes) ? decoded.scopes : [],
|
|
475
|
-
...(typeof decoded.credentialId === 'string' && decoded.credentialId.length > 0
|
|
476
|
-
? { credentialId: decoded.credentialId }
|
|
477
|
-
: {}),
|
|
478
469
|
};
|
|
479
470
|
if (debug) {
|
|
480
471
|
logger.debug(`[oxy.auth] Service token OK app=${decoded.appName} delegateUser=${oxyUserId || '(none)'}`, {
|
package/dist/esm/mixins/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { OxyServicesBase } from '../OxyServices.base.js';
|
|
8
8
|
import { OxyServicesAuthMixin } from './OxyServices.auth.js';
|
|
9
9
|
import { OxyServicesFedCMMixin } from './OxyServices.fedcm.js';
|
|
10
|
-
import {
|
|
10
|
+
import { OxyServicesSilentAuthMixin } from './OxyServices.silent.js';
|
|
11
11
|
import { OxyServicesRedirectAuthMixin } from './OxyServices.redirect.js';
|
|
12
12
|
import { OxyServicesSsoMixin } from './OxyServices.sso.js';
|
|
13
13
|
import { OxyServicesUserMixin } from './OxyServices.user.js';
|
|
@@ -33,7 +33,7 @@ import { OxyServicesAppDataMixin } from './OxyServices.appData.js';
|
|
|
33
33
|
*
|
|
34
34
|
* Order matters for dependencies:
|
|
35
35
|
* 1. Base auth mixin first (required by all others)
|
|
36
|
-
* 2. Cross-domain auth mixins (FedCM,
|
|
36
|
+
* 2. Cross-domain auth mixins (FedCM, silent iframe, Redirect)
|
|
37
37
|
* 3. User mixin (requires auth)
|
|
38
38
|
* 4. Feature mixins (can depend on user)
|
|
39
39
|
* 5. Utility mixin last (augments all)
|
|
@@ -45,13 +45,12 @@ const MIXIN_PIPELINE = [
|
|
|
45
45
|
OxyServicesAuthMixin,
|
|
46
46
|
// Cross-domain authentication (web-only)
|
|
47
47
|
// - FedCM: Modern browser-native identity federation (Google-style)
|
|
48
|
-
// -
|
|
48
|
+
// - Silent: iframe-based restore for first-party IdP hosts
|
|
49
49
|
// - Redirect: Traditional redirect-based authentication
|
|
50
50
|
OxyServicesFedCMMixin,
|
|
51
|
-
|
|
51
|
+
OxyServicesSilentAuthMixin,
|
|
52
52
|
OxyServicesRedirectAuthMixin,
|
|
53
|
-
// Central cross-domain SSO (opaque-code exchange).
|
|
54
|
-
// reuse the popup mixin's secure-random `generateState()`.
|
|
53
|
+
// Central cross-domain SSO (opaque-code exchange).
|
|
55
54
|
OxyServicesSsoMixin,
|
|
56
55
|
// User management (requires auth)
|
|
57
56
|
OxyServicesUserMixin,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @oxyhq/core/server — Server-only utilities for Oxy backends
|
|
3
|
+
*
|
|
4
|
+
* This subpath export provides Express middleware and Node.js-specific
|
|
5
|
+
* utilities that are not available in React Native or browser environments.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { createOxyRateLimit } from '@oxyhq/core/server';
|
|
10
|
+
* import { oxyClient } from '@oxyhq/core';
|
|
11
|
+
*
|
|
12
|
+
* const oxy = oxyClient({ apiUrl: 'https://api.oxy.so' });
|
|
13
|
+
*
|
|
14
|
+
* app.use(createOxyRateLimit(oxy, { store: redisStore }));
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export { createOxyRateLimit } from './rateLimit.js';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import rateLimit from 'express-rate-limit';
|
|
2
|
+
/**
|
|
3
|
+
* Built-in exemptions. A media app's cover-art/avatar fan-out and HLS
|
|
4
|
+
* sub-requests must not consume the coarse global budget; health probes from
|
|
5
|
+
* the load balancer must never be limited; CORS preflight is not a real call.
|
|
6
|
+
*/
|
|
7
|
+
function isBuiltInExempt(req) {
|
|
8
|
+
const path = req.path;
|
|
9
|
+
return (req.method === 'OPTIONS' ||
|
|
10
|
+
path.startsWith('/files/upload') ||
|
|
11
|
+
path.includes('/images/') ||
|
|
12
|
+
path.includes('/media/') ||
|
|
13
|
+
path.startsWith('/api/stream/') ||
|
|
14
|
+
path.includes('/stream/') ||
|
|
15
|
+
path === '/health' ||
|
|
16
|
+
path.endsWith('/health'));
|
|
17
|
+
}
|
|
18
|
+
/** IPv6-safe IP key generator (replaces colons to avoid Redis namespace issues). */
|
|
19
|
+
function ipKeyGenerator(ip) {
|
|
20
|
+
return ip.replace(/:/g, '_');
|
|
21
|
+
}
|
|
22
|
+
/** Resolve the rate-limit key: per authenticated user, else per (IPv6-safe) IP. */
|
|
23
|
+
function resolveKey(req) {
|
|
24
|
+
const userId = req.userId ?? req.user?.id ?? req.user?._id;
|
|
25
|
+
if (userId) {
|
|
26
|
+
return `user:${userId}`;
|
|
27
|
+
}
|
|
28
|
+
const ip = req.ip || req.socket?.remoteAddress || 'unknown';
|
|
29
|
+
return ipKeyGenerator(ip);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the composed Oxy rate-limit middleware. See module docs for rationale.
|
|
33
|
+
*/
|
|
34
|
+
export function createOxyRateLimit(oxy, options = {}) {
|
|
35
|
+
const { authenticatedMax = 5000, anonymousMax = 600, windowMs = 15 * 60 * 1000, store, exempt, message = 'Too many requests, please try again later.', auth, } = options;
|
|
36
|
+
// Idempotent optional-auth resolver. Reuses the SAME session resolution as
|
|
37
|
+
// every protected route, so the limiter keys by the real user identity.
|
|
38
|
+
const resolveSession = oxy.auth({ ...auth, optional: true });
|
|
39
|
+
const skip = (req) => isBuiltInExempt(req) || (exempt ? exempt(req) : false);
|
|
40
|
+
const limiter = rateLimit({
|
|
41
|
+
windowMs,
|
|
42
|
+
...(store ? { store } : {}),
|
|
43
|
+
max: ((req) => {
|
|
44
|
+
const authed = req;
|
|
45
|
+
const userId = authed.userId ?? authed.user?.id ?? authed.user?._id;
|
|
46
|
+
return userId ? authenticatedMax : anonymousMax;
|
|
47
|
+
}),
|
|
48
|
+
keyGenerator: ((req) => resolveKey(req)),
|
|
49
|
+
message,
|
|
50
|
+
standardHeaders: true,
|
|
51
|
+
legacyHeaders: false,
|
|
52
|
+
skip: skip,
|
|
53
|
+
});
|
|
54
|
+
return ((req, res, next) => {
|
|
55
|
+
// Skipped paths bypass BOTH session resolution and limiting — cheap and
|
|
56
|
+
// safe for static/streaming/health traffic.
|
|
57
|
+
if (skip(req)) {
|
|
58
|
+
next();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
resolveSession(req, res, (err) => {
|
|
62
|
+
if (err) {
|
|
63
|
+
// Optional auth never rejects; a token error just means "anonymous".
|
|
64
|
+
// Swallow the error and continue to limit as anonymous.
|
|
65
|
+
next();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
limiter(req, res, next);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -51,7 +51,7 @@ export const debugError = (prefix, ...args) => {
|
|
|
51
51
|
};
|
|
52
52
|
/**
|
|
53
53
|
* Create a namespaced debug logger
|
|
54
|
-
* @param namespace - Logger namespace (e.g., 'FedCM', '
|
|
54
|
+
* @param namespace - Logger namespace (e.g., 'FedCM', 'SilentAuth')
|
|
55
55
|
* @returns Object with log, warn, error methods
|
|
56
56
|
*
|
|
57
57
|
* @example
|
|
@@ -138,10 +138,10 @@ export const mergeAccountsFromRefreshAll = (stored, fresh) => {
|
|
|
138
138
|
}
|
|
139
139
|
const merged = fresh.map((entry) => {
|
|
140
140
|
const previous = storedByAuthuser.get(entry.authuser);
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
141
|
+
// Preserve any previously cached identity for a slot that arrives
|
|
142
|
+
// without a user shape rather than overwriting it with blanks, and let
|
|
143
|
+
// AuthManager's getCurrentUser() hydration refresh it on the next
|
|
144
|
+
// snapshot.
|
|
145
145
|
const wireUser = entry.user;
|
|
146
146
|
const username = wireUser?.username ?? previous?.username ?? '';
|
|
147
147
|
const displayName = getAccountDisplayName({
|
|
@@ -22,25 +22,32 @@ export class AuthenticationFailedError extends Error {
|
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Ensures a valid token exists before making authenticated API calls.
|
|
25
|
-
* If no valid token exists
|
|
26
|
-
*
|
|
25
|
+
* If no valid token exists, callers may provide a session synchronizer that
|
|
26
|
+
* uses the platform-appropriate new flow (cookie restore, device claim, or
|
|
27
|
+
* native secure restore). This helper never exchanges a session id for a token.
|
|
27
28
|
*
|
|
28
29
|
* @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
|
|
29
30
|
*/
|
|
30
|
-
export async function ensureValidToken(oxyServices,
|
|
31
|
-
if (oxyServices.hasValidToken()
|
|
31
|
+
export async function ensureValidToken(oxyServices, _activeSessionId, syncSession) {
|
|
32
|
+
if (oxyServices.hasValidToken()) {
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
if (syncSession) {
|
|
36
|
+
try {
|
|
37
|
+
await syncSession();
|
|
38
|
+
if (oxyServices.hasValidToken()) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (syncError) {
|
|
43
|
+
const errorMessage = syncError instanceof Error ? syncError.message : String(syncError);
|
|
44
|
+
if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
|
|
45
|
+
throw new SessionSyncRequiredError();
|
|
46
|
+
}
|
|
47
|
+
throw syncError;
|
|
41
48
|
}
|
|
42
|
-
throw tokenError;
|
|
43
49
|
}
|
|
50
|
+
throw new SessionSyncRequiredError('No active access token is available. Sync the session before calling authenticated APIs.');
|
|
44
51
|
}
|
|
45
52
|
/**
|
|
46
53
|
* Checks if an error is an authentication error (401 or auth-related message)
|
|
@@ -70,10 +77,9 @@ export async function withAuthErrorHandling(apiCall, options) {
|
|
|
70
77
|
if (!isAuthenticationError(error)) {
|
|
71
78
|
throw error;
|
|
72
79
|
}
|
|
73
|
-
if (options?.syncSession && options?.
|
|
80
|
+
if (options?.syncSession && options?.oxyServices) {
|
|
74
81
|
try {
|
|
75
82
|
await options.syncSession();
|
|
76
|
-
await options.oxyServices.getTokenBySession(options.activeSessionId);
|
|
77
83
|
return await apiCall();
|
|
78
84
|
}
|
|
79
85
|
catch {
|
|
@@ -96,7 +102,7 @@ export async function withAuthErrorHandling(apiCall, options) {
|
|
|
96
102
|
* ```
|
|
97
103
|
*/
|
|
98
104
|
export async function authenticatedApiCall(oxyServices, activeSessionId, apiCall, syncSession) {
|
|
99
|
-
await ensureValidToken(oxyServices, activeSessionId);
|
|
105
|
+
await ensureValidToken(oxyServices, activeSessionId, syncSession);
|
|
100
106
|
return withAuthErrorHandling(apiCall, {
|
|
101
107
|
syncSession,
|
|
102
108
|
activeSessionId,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* On a fresh page load / app launch the SDK may have several ways to recover an
|
|
6
6
|
* existing session (silent FedCM, a persisted refresh token, a cross-domain
|
|
7
|
-
* claim,
|
|
7
|
+
* claim, a redirect SSO return, ...). They must be attempted in a deterministic
|
|
8
8
|
* order*, and the FIRST one that yields a session wins — every later step is
|
|
9
9
|
* skipped. This module encodes exactly that contract and nothing else.
|
|
10
10
|
*
|
|
@@ -81,8 +81,8 @@ export async function runColdBoot(options) {
|
|
|
81
81
|
}
|
|
82
82
|
let result;
|
|
83
83
|
try {
|
|
84
|
-
// Without a deadline
|
|
85
|
-
//
|
|
84
|
+
// Without a deadline, await the step directly. With a deadline, race
|
|
85
|
+
// the step against the shared deadline. The
|
|
86
86
|
// step's `run()` still STARTS synchronously up to its first `await`
|
|
87
87
|
// (so a terminal step's synchronous navigation side effect always
|
|
88
88
|
// executes), but a non-settling step can no longer block the loop —
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Clerk-style multi-domain SSO depends on the IdP being reachable on a
|
|
9
9
|
* subdomain of the RP's own apex (e.g. `auth.mention.earth` CNAMEd to the
|
|
10
10
|
* central Oxy IdP). That way every FedCM endpoint, the session cookie,
|
|
11
|
-
* and any
|
|
11
|
+
* and any redirect target are same-site with the RP — the only way
|
|
12
12
|
* to get first-party cookies in Safari ITP and Firefox Total Cookie
|
|
13
13
|
* Protection.
|
|
14
14
|
*
|