@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
package/src/mixins/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { OxyServicesBase } from '../OxyServices.base';
|
|
9
9
|
import { OxyServicesAuthMixin } from './OxyServices.auth';
|
|
10
10
|
import { OxyServicesFedCMMixin } from './OxyServices.fedcm';
|
|
11
|
-
import {
|
|
11
|
+
import { OxyServicesSilentAuthMixin } from './OxyServices.silent';
|
|
12
12
|
import { OxyServicesRedirectAuthMixin } from './OxyServices.redirect';
|
|
13
13
|
import { OxyServicesSsoMixin } from './OxyServices.sso';
|
|
14
14
|
import { OxyServicesUserMixin } from './OxyServices.user';
|
|
@@ -42,7 +42,7 @@ import { OxyServicesAppDataMixin } from './OxyServices.appData';
|
|
|
42
42
|
type AllMixinInstances =
|
|
43
43
|
& InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>>
|
|
44
44
|
& InstanceType<ReturnType<typeof OxyServicesFedCMMixin<typeof OxyServicesBase>>>
|
|
45
|
-
& InstanceType<ReturnType<typeof
|
|
45
|
+
& InstanceType<ReturnType<typeof OxyServicesSilentAuthMixin<typeof OxyServicesBase>>>
|
|
46
46
|
& InstanceType<ReturnType<typeof OxyServicesRedirectAuthMixin<typeof OxyServicesBase>>>
|
|
47
47
|
& InstanceType<ReturnType<typeof OxyServicesSsoMixin<typeof OxyServicesBase>>>
|
|
48
48
|
& InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>>
|
|
@@ -84,7 +84,7 @@ type MixinFunction = (Base: new (...args: unknown[]) => OxyServicesBase) => new
|
|
|
84
84
|
*
|
|
85
85
|
* Order matters for dependencies:
|
|
86
86
|
* 1. Base auth mixin first (required by all others)
|
|
87
|
-
* 2. Cross-domain auth mixins (FedCM,
|
|
87
|
+
* 2. Cross-domain auth mixins (FedCM, silent iframe, Redirect)
|
|
88
88
|
* 3. User mixin (requires auth)
|
|
89
89
|
* 4. Feature mixins (can depend on user)
|
|
90
90
|
* 5. Utility mixin last (augments all)
|
|
@@ -97,14 +97,13 @@ const MIXIN_PIPELINE: MixinFunction[] = [
|
|
|
97
97
|
|
|
98
98
|
// Cross-domain authentication (web-only)
|
|
99
99
|
// - FedCM: Modern browser-native identity federation (Google-style)
|
|
100
|
-
// -
|
|
100
|
+
// - Silent: iframe-based restore for first-party IdP hosts
|
|
101
101
|
// - Redirect: Traditional redirect-based authentication
|
|
102
102
|
OxyServicesFedCMMixin,
|
|
103
|
-
|
|
103
|
+
OxyServicesSilentAuthMixin,
|
|
104
104
|
OxyServicesRedirectAuthMixin,
|
|
105
105
|
|
|
106
|
-
// Central cross-domain SSO (opaque-code exchange).
|
|
107
|
-
// reuse the popup mixin's secure-random `generateState()`.
|
|
106
|
+
// Central cross-domain SSO (opaque-code exchange).
|
|
108
107
|
OxyServicesSsoMixin,
|
|
109
108
|
|
|
110
109
|
// User management (requires auth)
|
|
@@ -155,4 +154,3 @@ export function composeOxyServices(): ComposedOxyServicesConstructor {
|
|
|
155
154
|
|
|
156
155
|
// Export the pipeline for testing/debugging
|
|
157
156
|
export { MIXIN_PIPELINE };
|
|
158
|
-
|
package/src/models/interfaces.ts
CHANGED
|
@@ -174,8 +174,6 @@ export interface UserPreferences {
|
|
|
174
174
|
|
|
175
175
|
export interface LoginResponse {
|
|
176
176
|
accessToken?: string;
|
|
177
|
-
refreshToken?: string;
|
|
178
|
-
token?: string; // For backwards compatibility
|
|
179
177
|
user: User;
|
|
180
178
|
message?: string;
|
|
181
179
|
}
|
|
@@ -661,15 +659,7 @@ export interface RefreshAllAccountUser {
|
|
|
661
659
|
|
|
662
660
|
/**
|
|
663
661
|
* One rotated account entry returned by `POST /auth/refresh-all`. `authuser` is
|
|
664
|
-
* the device-local slot index (0..N-1) the cookie was bound to.
|
|
665
|
-
* un-suffixed `oxy_rt` cookie yields `authuser: null` server-side, but the SDK
|
|
666
|
-
* normalises that to `0` before exposing it (the chooser always operates on
|
|
667
|
-
* numeric indices).
|
|
668
|
-
*
|
|
669
|
-
* `user` is `null` only on the SDK-side synthesised legacy fallback (when the
|
|
670
|
-
* server is too old to support `/auth/refresh-all` and we wrap a
|
|
671
|
-
* `/auth/refresh` response — that endpoint does not project a user shape).
|
|
672
|
-
* On the modern path every accepted entry carries a non-null user.
|
|
662
|
+
* the device-local slot index (0..N-1) the cookie was bound to.
|
|
673
663
|
*/
|
|
674
664
|
export interface RefreshAllAccount {
|
|
675
665
|
authuser: number;
|
|
@@ -689,13 +679,12 @@ export interface RefreshAllResponse {
|
|
|
689
679
|
}
|
|
690
680
|
|
|
691
681
|
/**
|
|
692
|
-
* Wire shape of `POST /auth/refresh` (single-
|
|
693
|
-
*
|
|
694
|
-
* the response
|
|
695
|
-
* `authuser: null`.
|
|
682
|
+
* Wire shape of `POST /auth/refresh` (single-slot refresh, optionally targeting
|
|
683
|
+
* a specific `?authuser=N` slot). The server always includes the numeric slot in
|
|
684
|
+
* the response.
|
|
696
685
|
*/
|
|
697
686
|
export interface RefreshCookieResponse {
|
|
698
687
|
accessToken: string;
|
|
699
688
|
expiresAt: string;
|
|
700
|
-
authuser: number
|
|
689
|
+
authuser: number;
|
|
701
690
|
}
|
package/src/models/session.ts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
|
|
18
|
+
export { createOxyRateLimit } from './rateLimit';
|
|
19
|
+
export type { OxyRateLimitOptions } from './rateLimit';
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { Request, RequestHandler } from 'express';
|
|
2
|
+
import rateLimit, { type Store } from 'express-rate-limit';
|
|
3
|
+
import type { OxyServices } from '../OxyServices';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Server-only rate limiting for Oxy backends.
|
|
7
|
+
*
|
|
8
|
+
* WHY THIS EXISTS
|
|
9
|
+
* ---------------
|
|
10
|
+
* Every Oxy backend previously shipped its own copy-pasted `security.ts`
|
|
11
|
+
* implementing the same per-user/per-IP rate limiter — and every copy carried
|
|
12
|
+
* the same latent bug: the limiter ran BEFORE the session was resolved, so
|
|
13
|
+
* `req.user` was always undefined inside it. Consequences:
|
|
14
|
+
* 1. Authenticated users got the low ANONYMOUS limit.
|
|
15
|
+
* 2. Requests were keyed by IP. Behind a shared load balancer (e.g. the AWS
|
|
16
|
+
* ALB) many users share one egress IP, so a single bucket was split across
|
|
17
|
+
* all of them → frequent, spurious HTTP 429s.
|
|
18
|
+
*
|
|
19
|
+
* `@oxyhq/core` already owns the session: `oxy.auth()` resolves `req.user` /
|
|
20
|
+
* `req.userId`. Per-user rate limiting is the same concern (session identity),
|
|
21
|
+
* so it belongs here — once — instead of being re-implemented per app.
|
|
22
|
+
*
|
|
23
|
+
* WHAT IT PROVIDES
|
|
24
|
+
* ----------------
|
|
25
|
+
* `createOxyRateLimit(oxy, options)` returns a SINGLE composed middleware that:
|
|
26
|
+
* 1. Resolves the user via `oxy.auth({ optional: true })` (idempotent — it
|
|
27
|
+
* skips re-verification if a prior middleware already set `req.user`).
|
|
28
|
+
* 2. Applies an `express-rate-limit` limiter keyed PER USER when
|
|
29
|
+
* authenticated, falling back to the (IPv6-safe) IP otherwise, with
|
|
30
|
+
* generous, media-app-realistic defaults and sensible exemptions.
|
|
31
|
+
*
|
|
32
|
+
* Mount it after CORS and before your routers:
|
|
33
|
+
* ```ts
|
|
34
|
+
* app.use(cors(...));
|
|
35
|
+
* app.use(oxy.rateLimit({ store })); // resolves session + limits per user
|
|
36
|
+
* app.use('/api', apiRouter);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** Minimal shape of the request after Oxy session resolution. */
|
|
41
|
+
interface OxyAuthedRequest extends Request {
|
|
42
|
+
userId?: string | null;
|
|
43
|
+
user?: { id?: string; _id?: string } | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface OxyRateLimitOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Max requests per window for AUTHENTICATED users (keyed per user).
|
|
49
|
+
* Default 5000 — ~5.5 req/s sustained, comfortable for a media client that
|
|
50
|
+
* fans out into many small requests per screen.
|
|
51
|
+
*/
|
|
52
|
+
authenticatedMax?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Max requests per window for ANONYMOUS callers (keyed per IP).
|
|
55
|
+
* Default 600 — enough to browse public pages while bounding abuse.
|
|
56
|
+
*/
|
|
57
|
+
anonymousMax?: number;
|
|
58
|
+
/** Rate-limit window in milliseconds. Default 15 minutes. */
|
|
59
|
+
windowMs?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Optional `express-rate-limit` store (e.g. a Redis store) for distributed
|
|
62
|
+
* limiting across instances. Defaults to the library's in-memory store.
|
|
63
|
+
*/
|
|
64
|
+
store?: Store;
|
|
65
|
+
/**
|
|
66
|
+
* Extra path predicates to exempt from limiting, in addition to the built-in
|
|
67
|
+
* exemptions (uploads, image proxy, streaming sub-requests, health probes,
|
|
68
|
+
* CORS preflight). Return `true` to skip limiting for the request.
|
|
69
|
+
*/
|
|
70
|
+
exempt?: (req: Request) => boolean;
|
|
71
|
+
/** Response message body sent with a 429. */
|
|
72
|
+
message?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Options forwarded to the internal `oxy.auth({ optional: true })` resolver
|
|
75
|
+
* (e.g. `{ jwtSecret }` to verify service tokens). `optional` is forced true.
|
|
76
|
+
*/
|
|
77
|
+
auth?: Parameters<OxyServices['auth']>[0];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Built-in exemptions. A media app's cover-art/avatar fan-out and HLS
|
|
82
|
+
* sub-requests must not consume the coarse global budget; health probes from
|
|
83
|
+
* the load balancer must never be limited; CORS preflight is not a real call.
|
|
84
|
+
*/
|
|
85
|
+
function isBuiltInExempt(req: Request): boolean {
|
|
86
|
+
const path = req.path;
|
|
87
|
+
return (
|
|
88
|
+
req.method === 'OPTIONS' ||
|
|
89
|
+
path.startsWith('/files/upload') ||
|
|
90
|
+
path.includes('/images/') ||
|
|
91
|
+
path.includes('/media/') ||
|
|
92
|
+
path.startsWith('/api/stream/') ||
|
|
93
|
+
path.includes('/stream/') ||
|
|
94
|
+
path === '/health' ||
|
|
95
|
+
path.endsWith('/health')
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** IPv6-safe IP key generator (replaces colons to avoid Redis namespace issues). */
|
|
100
|
+
function ipKeyGenerator(ip: string): string {
|
|
101
|
+
return ip.replace(/:/g, '_');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolve the rate-limit key: per authenticated user, else per (IPv6-safe) IP. */
|
|
105
|
+
function resolveKey(req: OxyAuthedRequest): string {
|
|
106
|
+
const userId = req.userId ?? req.user?.id ?? req.user?._id;
|
|
107
|
+
if (userId) {
|
|
108
|
+
return `user:${userId}`;
|
|
109
|
+
}
|
|
110
|
+
const ip = req.ip || (req.socket as any)?.remoteAddress || 'unknown';
|
|
111
|
+
return ipKeyGenerator(ip);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build the composed Oxy rate-limit middleware. See module docs for rationale.
|
|
116
|
+
*/
|
|
117
|
+
export function createOxyRateLimit(
|
|
118
|
+
oxy: OxyServices,
|
|
119
|
+
options: OxyRateLimitOptions = {},
|
|
120
|
+
): RequestHandler {
|
|
121
|
+
const {
|
|
122
|
+
authenticatedMax = 5000,
|
|
123
|
+
anonymousMax = 600,
|
|
124
|
+
windowMs = 15 * 60 * 1000,
|
|
125
|
+
store,
|
|
126
|
+
exempt,
|
|
127
|
+
message = 'Too many requests, please try again later.',
|
|
128
|
+
auth,
|
|
129
|
+
} = options;
|
|
130
|
+
|
|
131
|
+
// Idempotent optional-auth resolver. Reuses the SAME session resolution as
|
|
132
|
+
// every protected route, so the limiter keys by the real user identity.
|
|
133
|
+
const resolveSession = oxy.auth({ ...auth, optional: true });
|
|
134
|
+
|
|
135
|
+
const skip = (req: Request): boolean =>
|
|
136
|
+
isBuiltInExempt(req) || (exempt ? exempt(req) : false);
|
|
137
|
+
|
|
138
|
+
const limiter = rateLimit({
|
|
139
|
+
windowMs,
|
|
140
|
+
...(store ? { store } : {}),
|
|
141
|
+
max: ((req: Request): number => {
|
|
142
|
+
const authed = req as OxyAuthedRequest;
|
|
143
|
+
const userId = authed.userId ?? authed.user?.id ?? authed.user?._id;
|
|
144
|
+
return userId ? authenticatedMax : anonymousMax;
|
|
145
|
+
}) as any,
|
|
146
|
+
keyGenerator: ((req: Request): string => resolveKey(req as OxyAuthedRequest)) as any,
|
|
147
|
+
message,
|
|
148
|
+
standardHeaders: true,
|
|
149
|
+
legacyHeaders: false,
|
|
150
|
+
skip: skip as any,
|
|
151
|
+
} as any);
|
|
152
|
+
|
|
153
|
+
return ((req: any, res: any, next: any) => {
|
|
154
|
+
// Skipped paths bypass BOTH session resolution and limiting — cheap and
|
|
155
|
+
// safe for static/streaming/health traffic.
|
|
156
|
+
if (skip(req)) {
|
|
157
|
+
next();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
resolveSession(req, res, (err?: unknown) => {
|
|
161
|
+
if (err) {
|
|
162
|
+
// Optional auth never rejects; a token error just means "anonymous".
|
|
163
|
+
// Swallow the error and continue to limit as anonymous.
|
|
164
|
+
next();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
limiter(req, res, next);
|
|
168
|
+
});
|
|
169
|
+
}) as RequestHandler;
|
|
170
|
+
}
|
|
@@ -57,7 +57,7 @@ export const debugError = (prefix: string, ...args: unknown[]): void => {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Create a namespaced debug logger
|
|
60
|
-
* @param namespace - Logger namespace (e.g., 'FedCM', '
|
|
60
|
+
* @param namespace - Logger namespace (e.g., 'FedCM', 'SilentAuth')
|
|
61
61
|
* @returns Object with log, warn, error methods
|
|
62
62
|
*
|
|
63
63
|
* @example
|
|
@@ -204,10 +204,10 @@ export const mergeAccountsFromRefreshAll = (
|
|
|
204
204
|
|
|
205
205
|
const merged: QuickAccount[] = fresh.map((entry) => {
|
|
206
206
|
const previous = storedByAuthuser.get(entry.authuser);
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
207
|
+
// Preserve any previously cached identity for a slot that arrives
|
|
208
|
+
// without a user shape rather than overwriting it with blanks, and let
|
|
209
|
+
// AuthManager's getCurrentUser() hydration refresh it on the next
|
|
210
|
+
// snapshot.
|
|
211
211
|
const wireUser = entry.user;
|
|
212
212
|
const username = wireUser?.username ?? previous?.username ?? '';
|
|
213
213
|
const displayName = getAccountDisplayName({
|
package/src/utils/authHelpers.ts
CHANGED
|
@@ -27,30 +27,39 @@ export class AuthenticationFailedError extends Error {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Ensures a valid token exists before making authenticated API calls.
|
|
30
|
-
* If no valid token exists
|
|
31
|
-
*
|
|
30
|
+
* If no valid token exists, callers may provide a session synchronizer that
|
|
31
|
+
* uses the platform-appropriate new flow (cookie restore, device claim, or
|
|
32
|
+
* native secure restore). This helper never exchanges a session id for a token.
|
|
32
33
|
*
|
|
33
34
|
* @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
|
|
34
35
|
*/
|
|
35
36
|
export async function ensureValidToken(
|
|
36
37
|
oxyServices: OxyServices,
|
|
37
|
-
|
|
38
|
+
_activeSessionId: string | null | undefined,
|
|
39
|
+
syncSession?: () => Promise<unknown>
|
|
38
40
|
): Promise<void> {
|
|
39
|
-
if (oxyServices.hasValidToken()
|
|
41
|
+
if (oxyServices.hasValidToken()) {
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
if (syncSession) {
|
|
46
|
+
try {
|
|
47
|
+
await syncSession();
|
|
48
|
+
if (oxyServices.hasValidToken()) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
} catch (syncError) {
|
|
52
|
+
const errorMessage = syncError instanceof Error ? syncError.message : String(syncError);
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
|
|
55
|
+
throw new SessionSyncRequiredError();
|
|
56
|
+
}
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
throw syncError;
|
|
59
|
+
}
|
|
53
60
|
}
|
|
61
|
+
|
|
62
|
+
throw new SessionSyncRequiredError('No active access token is available. Sync the session before calling authenticated APIs.');
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
/**
|
|
@@ -98,10 +107,9 @@ export async function withAuthErrorHandling<T>(
|
|
|
98
107
|
throw error;
|
|
99
108
|
}
|
|
100
109
|
|
|
101
|
-
if (options?.syncSession && options?.
|
|
110
|
+
if (options?.syncSession && options?.oxyServices) {
|
|
102
111
|
try {
|
|
103
112
|
await options.syncSession();
|
|
104
|
-
await options.oxyServices.getTokenBySession(options.activeSessionId);
|
|
105
113
|
return await apiCall();
|
|
106
114
|
} catch {
|
|
107
115
|
throw new AuthenticationFailedError();
|
|
@@ -130,7 +138,7 @@ export async function authenticatedApiCall<T>(
|
|
|
130
138
|
apiCall: () => Promise<T>,
|
|
131
139
|
syncSession?: () => Promise<unknown>
|
|
132
140
|
): Promise<T> {
|
|
133
|
-
await ensureValidToken(oxyServices, activeSessionId);
|
|
141
|
+
await ensureValidToken(oxyServices, activeSessionId, syncSession);
|
|
134
142
|
|
|
135
143
|
return withAuthErrorHandling(apiCall, {
|
|
136
144
|
syncSession,
|
package/src/utils/coldBoot.ts
CHANGED
|
@@ -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
|
*
|
|
@@ -112,7 +112,7 @@ export interface RunColdBootOptions<S> {
|
|
|
112
112
|
* Per-step timeouts inside `run()` remain the first line of defense and
|
|
113
113
|
* should keep every step well under this budget on a healthy load; this only
|
|
114
114
|
* trips when one of them regresses (the production FedCM-silent hang). When
|
|
115
|
-
* omitted there is
|
|
115
|
+
* omitted there is no overall deadline.
|
|
116
116
|
*/
|
|
117
117
|
readonly overallDeadlineMs?: number;
|
|
118
118
|
/**
|
|
@@ -178,8 +178,8 @@ export async function runColdBoot<S>(
|
|
|
178
178
|
|
|
179
179
|
let result: ColdBootStepResult<S> | typeof DEADLINE_EXPIRED;
|
|
180
180
|
try {
|
|
181
|
-
// Without a deadline
|
|
182
|
-
//
|
|
181
|
+
// Without a deadline, await the step directly. With a deadline, race
|
|
182
|
+
// the step against the shared deadline. The
|
|
183
183
|
// step's `run()` still STARTS synchronously up to its first `await`
|
|
184
184
|
// (so a terminal step's synchronous navigation side effect always
|
|
185
185
|
// 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
|
*
|