@oxyhq/core 3.4.1 → 3.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/AuthManager.js +91 -319
  3. package/dist/cjs/CrossDomainAuth.js +19 -106
  4. package/dist/cjs/HttpService.js +49 -73
  5. package/dist/cjs/OxyServices.base.js +2 -2
  6. package/dist/cjs/i18n/index.js +7 -1
  7. package/dist/cjs/i18n/locales/ar-SA.json +18 -2
  8. package/dist/cjs/i18n/locales/ca-ES.json +18 -2
  9. package/dist/cjs/i18n/locales/de-DE.json +18 -2
  10. package/dist/cjs/i18n/locales/en-US.json +16 -2
  11. package/dist/cjs/i18n/locales/es-ES.json +16 -2
  12. package/dist/cjs/i18n/locales/fr-FR.json +18 -2
  13. package/dist/cjs/i18n/locales/it-IT.json +18 -2
  14. package/dist/cjs/i18n/locales/ja-JP.json +18 -2
  15. package/dist/cjs/i18n/locales/ko-KR.json +18 -2
  16. package/dist/cjs/i18n/locales/locales/ar-SA.json +18 -2
  17. package/dist/cjs/i18n/locales/locales/ca-ES.json +18 -2
  18. package/dist/cjs/i18n/locales/locales/de-DE.json +18 -2
  19. package/dist/cjs/i18n/locales/locales/en-US.json +17 -3
  20. package/dist/cjs/i18n/locales/locales/es-ES.json +16 -2
  21. package/dist/cjs/i18n/locales/locales/fr-FR.json +18 -2
  22. package/dist/cjs/i18n/locales/locales/it-IT.json +18 -2
  23. package/dist/cjs/i18n/locales/locales/ja-JP.json +18 -2
  24. package/dist/cjs/i18n/locales/locales/ko-KR.json +18 -2
  25. package/dist/cjs/i18n/locales/locales/pt-PT.json +18 -2
  26. package/dist/cjs/i18n/locales/locales/zh-CN.json +18 -2
  27. package/dist/cjs/i18n/locales/pt-PT.json +18 -2
  28. package/dist/cjs/i18n/locales/zh-CN.json +18 -2
  29. package/dist/cjs/mixins/OxyServices.auth.js +20 -63
  30. package/dist/cjs/mixins/OxyServices.fedcm.js +10 -12
  31. package/dist/cjs/mixins/OxyServices.popup.js +50 -299
  32. package/dist/cjs/mixins/OxyServices.redirect.js +84 -348
  33. package/dist/cjs/mixins/OxyServices.silent.js +204 -0
  34. package/dist/cjs/mixins/OxyServices.sso.js +4 -5
  35. package/dist/cjs/mixins/OxyServices.utility.js +6 -15
  36. package/dist/cjs/mixins/index.js +5 -6
  37. package/dist/cjs/server/index.js +21 -0
  38. package/dist/cjs/server/rateLimit.js +77 -0
  39. package/dist/cjs/shared/utils/debugUtils.js +1 -1
  40. package/dist/cjs/utils/accountUtils.js +4 -4
  41. package/dist/cjs/utils/authHelpers.js +21 -15
  42. package/dist/cjs/utils/coldBoot.js +3 -3
  43. package/dist/cjs/utils/fapiAutoDetect.js +1 -1
  44. package/dist/esm/.tsbuildinfo +1 -1
  45. package/dist/esm/AuthManager.js +91 -319
  46. package/dist/esm/CrossDomainAuth.js +19 -106
  47. package/dist/esm/HttpService.js +49 -73
  48. package/dist/esm/OxyServices.base.js +2 -2
  49. package/dist/esm/i18n/index.js +7 -1
  50. package/dist/esm/i18n/locales/ar-SA.json +18 -2
  51. package/dist/esm/i18n/locales/ca-ES.json +18 -2
  52. package/dist/esm/i18n/locales/de-DE.json +18 -2
  53. package/dist/esm/i18n/locales/en-US.json +16 -2
  54. package/dist/esm/i18n/locales/es-ES.json +16 -2
  55. package/dist/esm/i18n/locales/fr-FR.json +18 -2
  56. package/dist/esm/i18n/locales/it-IT.json +18 -2
  57. package/dist/esm/i18n/locales/ja-JP.json +18 -2
  58. package/dist/esm/i18n/locales/ko-KR.json +18 -2
  59. package/dist/esm/i18n/locales/locales/ar-SA.json +18 -2
  60. package/dist/esm/i18n/locales/locales/ca-ES.json +18 -2
  61. package/dist/esm/i18n/locales/locales/de-DE.json +18 -2
  62. package/dist/esm/i18n/locales/locales/en-US.json +17 -3
  63. package/dist/esm/i18n/locales/locales/es-ES.json +16 -2
  64. package/dist/esm/i18n/locales/locales/fr-FR.json +18 -2
  65. package/dist/esm/i18n/locales/locales/it-IT.json +18 -2
  66. package/dist/esm/i18n/locales/locales/ja-JP.json +18 -2
  67. package/dist/esm/i18n/locales/locales/ko-KR.json +18 -2
  68. package/dist/esm/i18n/locales/locales/pt-PT.json +18 -2
  69. package/dist/esm/i18n/locales/locales/zh-CN.json +18 -2
  70. package/dist/esm/i18n/locales/pt-PT.json +18 -2
  71. package/dist/esm/i18n/locales/zh-CN.json +18 -2
  72. package/dist/esm/mixins/OxyServices.auth.js +20 -63
  73. package/dist/esm/mixins/OxyServices.fedcm.js +10 -12
  74. package/dist/esm/mixins/OxyServices.popup.js +52 -301
  75. package/dist/esm/mixins/OxyServices.redirect.js +84 -349
  76. package/dist/esm/mixins/OxyServices.silent.js +202 -0
  77. package/dist/esm/mixins/OxyServices.sso.js +4 -5
  78. package/dist/esm/mixins/OxyServices.utility.js +6 -15
  79. package/dist/esm/mixins/index.js +5 -6
  80. package/dist/esm/server/index.js +17 -0
  81. package/dist/esm/server/rateLimit.js +71 -0
  82. package/dist/esm/shared/utils/debugUtils.js +1 -1
  83. package/dist/esm/utils/accountUtils.js +4 -4
  84. package/dist/esm/utils/authHelpers.js +21 -15
  85. package/dist/esm/utils/coldBoot.js +3 -3
  86. package/dist/esm/utils/fapiAutoDetect.js +1 -1
  87. package/dist/types/.tsbuildinfo +1 -1
  88. package/dist/types/AuthManager.d.ts +26 -53
  89. package/dist/types/AuthManagerTypes.d.ts +5 -9
  90. package/dist/types/CrossDomainAuth.d.ts +13 -52
  91. package/dist/types/HttpService.d.ts +9 -8
  92. package/dist/types/OxyServices.base.d.ts +1 -1
  93. package/dist/types/OxyServices.d.ts +4 -10
  94. package/dist/types/index.d.ts +1 -1
  95. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -1
  96. package/dist/types/mixins/OxyServices.appData.d.ts +1 -1
  97. package/dist/types/mixins/OxyServices.applications.d.ts +1 -1
  98. package/dist/types/mixins/OxyServices.assets.d.ts +1 -1
  99. package/dist/types/mixins/OxyServices.auth.d.ts +10 -31
  100. package/dist/types/mixins/OxyServices.contacts.d.ts +1 -1
  101. package/dist/types/mixins/OxyServices.devices.d.ts +1 -1
  102. package/dist/types/mixins/OxyServices.features.d.ts +1 -1
  103. package/dist/types/mixins/OxyServices.fedcm.d.ts +5 -5
  104. package/dist/types/mixins/OxyServices.language.d.ts +1 -1
  105. package/dist/types/mixins/OxyServices.location.d.ts +1 -1
  106. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -1
  107. package/dist/types/mixins/OxyServices.payment.d.ts +1 -1
  108. package/dist/types/mixins/OxyServices.popup.d.ts +18 -120
  109. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -1
  110. package/dist/types/mixins/OxyServices.redirect.d.ts +13 -174
  111. package/dist/types/mixins/OxyServices.reputation.d.ts +1 -1
  112. package/dist/types/mixins/OxyServices.security.d.ts +1 -1
  113. package/dist/types/mixins/OxyServices.silent.d.ts +131 -0
  114. package/dist/types/mixins/OxyServices.sso.d.ts +4 -5
  115. package/dist/types/mixins/OxyServices.topics.d.ts +1 -1
  116. package/dist/types/mixins/OxyServices.user.d.ts +1 -1
  117. package/dist/types/mixins/OxyServices.utility.d.ts +3 -8
  118. package/dist/types/mixins/OxyServices.workspaces.d.ts +1 -1
  119. package/dist/types/mixins/index.d.ts +3 -3
  120. package/dist/types/models/interfaces.d.ts +5 -16
  121. package/dist/types/models/session.d.ts +0 -2
  122. package/dist/types/server/index.d.ts +18 -0
  123. package/dist/types/server/rateLimit.d.ts +40 -0
  124. package/dist/types/shared/utils/debugUtils.d.ts +1 -1
  125. package/dist/types/utils/authHelpers.d.ts +4 -3
  126. package/dist/types/utils/coldBoot.d.ts +2 -2
  127. package/dist/types/utils/fapiAutoDetect.d.ts +1 -1
  128. package/package.json +24 -2
  129. package/src/AuthManager.ts +100 -370
  130. package/src/AuthManagerTypes.ts +5 -9
  131. package/src/CrossDomainAuth.ts +22 -129
  132. package/src/HttpService.ts +55 -73
  133. package/src/OxyServices.base.ts +2 -3
  134. package/src/OxyServices.ts +9 -11
  135. package/src/__tests__/authManager.cookiePath.test.ts +19 -17
  136. package/src/__tests__/authManager.security.test.ts +7 -3
  137. package/src/__tests__/crossDomainAuth.test.ts +26 -118
  138. package/src/i18n/index.ts +7 -1
  139. package/src/i18n/locales/ar-SA.json +18 -2
  140. package/src/i18n/locales/ca-ES.json +18 -2
  141. package/src/i18n/locales/de-DE.json +18 -2
  142. package/src/i18n/locales/en-US.json +17 -3
  143. package/src/i18n/locales/es-ES.json +16 -2
  144. package/src/i18n/locales/fr-FR.json +18 -2
  145. package/src/i18n/locales/it-IT.json +18 -2
  146. package/src/i18n/locales/ja-JP.json +18 -2
  147. package/src/i18n/locales/ko-KR.json +18 -2
  148. package/src/i18n/locales/pt-PT.json +18 -2
  149. package/src/i18n/locales/zh-CN.json +18 -2
  150. package/src/index.ts +1 -1
  151. package/src/mixins/OxyServices.auth.ts +23 -75
  152. package/src/mixins/OxyServices.fedcm.ts +10 -12
  153. package/src/mixins/OxyServices.redirect.ts +82 -371
  154. package/src/mixins/OxyServices.silent.ts +272 -0
  155. package/src/mixins/OxyServices.sso.ts +5 -6
  156. package/src/mixins/OxyServices.utility.ts +9 -22
  157. package/src/mixins/__tests__/appData.test.ts +1 -1
  158. package/src/mixins/__tests__/onTokensChanged.test.ts +1 -1
  159. package/src/mixins/__tests__/reputation.test.ts +1 -1
  160. package/src/mixins/__tests__/serviceAuth.test.ts +7 -5
  161. package/src/mixins/__tests__/silent.test.ts +102 -0
  162. package/src/mixins/__tests__/verifyChallenge.test.ts +9 -14
  163. package/src/mixins/index.ts +6 -8
  164. package/src/models/interfaces.ts +5 -16
  165. package/src/models/session.ts +1 -3
  166. package/src/server/index.ts +19 -0
  167. package/src/server/rateLimit.ts +170 -0
  168. package/src/shared/utils/debugUtils.ts +1 -1
  169. package/src/utils/accountUtils.ts +4 -4
  170. package/src/utils/authHelpers.ts +23 -15
  171. package/src/utils/coldBoot.ts +4 -4
  172. package/src/utils/fapiAutoDetect.ts +1 -1
  173. package/src/mixins/OxyServices.popup.ts +0 -631
  174. package/src/mixins/__tests__/popup.test.ts +0 -374
@@ -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 { OxyServicesPopupAuthMixin } from './OxyServices.popup';
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 OxyServicesPopupAuthMixin<typeof OxyServicesBase>>>
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, Popup, Redirect)
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
- // - Popup: OAuth2-style popup authentication
100
+ // - Silent: iframe-based restore for first-party IdP hosts
101
101
  // - Redirect: Traditional redirect-based authentication
102
102
  OxyServicesFedCMMixin,
103
- OxyServicesPopupAuthMixin,
103
+ OxyServicesSilentAuthMixin,
104
104
  OxyServicesRedirectAuthMixin,
105
105
 
106
- // Central cross-domain SSO (opaque-code exchange). After Popup so it can
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
-
@@ -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. The legacy
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-account refresh, optionally
693
- * targeting a specific `?authuser=N` slot). The server includes `authuser` in
694
- * the response when an indexed slot was rotated; the legacy slot yields
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 | null;
689
+ authuser: number;
701
690
  }
@@ -33,6 +33,4 @@ export interface SessionLoginResponse {
33
33
  user: MinimalUserData;
34
34
  /** JWT access token for API authentication */
35
35
  accessToken?: string;
36
- /** Refresh token for obtaining new access tokens */
37
- refreshToken?: string;
38
- }
36
+ }
@@ -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.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
+ },
146
+ keyGenerator: (req: Request): string => resolveKey(req as OxyAuthedRequest),
147
+ message,
148
+ standardHeaders: true,
149
+ legacyHeaders: false,
150
+ skip,
151
+ });
152
+
153
+ return (req, res, next) => {
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
+ };
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', 'PopupAuth')
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
- // `entry.user` is null on the SDK legacy-fallback path; preserve any
208
- // previously cached identity for that slot rather than overwriting
209
- // it with blanks, and let the AuthManager's getCurrentUser() hydration
210
- // refresh it on the next snapshot.
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({
@@ -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 and an active session ID is available,
31
- * attempts to refresh the token using the session.
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
- activeSessionId: string | null | undefined
38
+ _activeSessionId: string | null | undefined,
39
+ syncSession?: () => Promise<unknown>
38
40
  ): Promise<void> {
39
- if (oxyServices.hasValidToken() || !activeSessionId) {
41
+ if (oxyServices.hasValidToken()) {
40
42
  return;
41
43
  }
42
44
 
43
- try {
44
- await oxyServices.getTokenBySession(activeSessionId);
45
- } catch (tokenError) {
46
- const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
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
- if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
49
- throw new SessionSyncRequiredError();
50
- }
54
+ if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
55
+ throw new SessionSyncRequiredError();
56
+ }
51
57
 
52
- throw tokenError;
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?.activeSessionId && options?.oxyServices) {
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,
@@ -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, an explicit popup flow, ). They must be attempted in a *deterministic
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 NO overall deadline (unchanged legacy behaviour).
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: legacy behaviour — await the step directly.
182
- // With a deadline: race the step against the shared deadline. The
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 popup/redirect target are same-site with the RP — the only way
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
  *