@oxyhq/core 12.11.1 → 13.2.0

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 (56) hide show
  1. package/README.md +36 -2
  2. package/dist/cjs/.tsbuildinfo +1 -1
  3. package/dist/cjs/index.js +4 -13
  4. package/dist/cjs/mixins/OxyServices.deviceBoot.js +0 -31
  5. package/dist/cjs/mixins/OxyServices.user.js +50 -44
  6. package/dist/cjs/server/index.js +9 -6
  7. package/dist/cjs/server/rateLimit.js +3 -0
  8. package/dist/cjs/server/securityHeaders.js +234 -0
  9. package/dist/cjs/session/accountDialogController.js +6 -8
  10. package/dist/cjs/utils/apiUtils.js +40 -10
  11. package/dist/cjs/utils/oauthPkce.js +1 -5
  12. package/dist/cjs/utils/officialOrigins.js +3 -73
  13. package/dist/esm/.tsbuildinfo +1 -1
  14. package/dist/esm/index.js +3 -4
  15. package/dist/esm/mixins/OxyServices.deviceBoot.js +1 -32
  16. package/dist/esm/mixins/OxyServices.user.js +51 -45
  17. package/dist/esm/server/index.js +4 -1
  18. package/dist/esm/server/rateLimit.js +3 -0
  19. package/dist/esm/server/securityHeaders.js +224 -0
  20. package/dist/esm/session/accountDialogController.js +6 -8
  21. package/dist/esm/utils/apiUtils.js +39 -10
  22. package/dist/esm/utils/oauthPkce.js +0 -4
  23. package/dist/esm/utils/officialOrigins.js +3 -68
  24. package/dist/types/.tsbuildinfo +1 -1
  25. package/dist/types/index.d.ts +5 -7
  26. package/dist/types/mixins/OxyServices.auth.d.ts +1 -12
  27. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +1 -5
  28. package/dist/types/mixins/OxyServices.user.d.ts +27 -6
  29. package/dist/types/server/index.d.ts +3 -1
  30. package/dist/types/server/securityHeaders.d.ts +154 -0
  31. package/dist/types/session/accountDialogController.d.ts +9 -15
  32. package/dist/types/utils/apiUtils.d.ts +48 -6
  33. package/dist/types/utils/oauthPkce.d.ts +11 -7
  34. package/dist/types/utils/officialOrigins.d.ts +3 -13
  35. package/package.json +10 -5
  36. package/src/index.ts +9 -14
  37. package/src/mixins/OxyServices.auth.ts +6 -13
  38. package/src/mixins/OxyServices.deviceBoot.ts +0 -47
  39. package/src/mixins/OxyServices.user.ts +60 -49
  40. package/src/mixins/__tests__/commonsSignIn.test.ts +9 -2
  41. package/src/mixins/__tests__/followGraphPagination.test.ts +250 -0
  42. package/src/server/__tests__/securityHeaders.test.ts +244 -0
  43. package/src/server/index.ts +17 -8
  44. package/src/server/rateLimit.ts +3 -0
  45. package/src/server/securityHeaders.ts +304 -0
  46. package/src/session/__tests__/accountDialogController.test.ts +3 -5
  47. package/src/session/accountDialogController.ts +12 -18
  48. package/src/utils/__tests__/officialOrigins.test.ts +0 -57
  49. package/src/utils/apiUtils.ts +64 -15
  50. package/src/utils/oauthPkce.ts +11 -9
  51. package/src/utils/officialOrigins.ts +3 -70
  52. package/dist/cjs/session/hubSync.js +0 -55
  53. package/dist/esm/session/hubSync.js +0 -51
  54. package/dist/types/session/hubSync.d.ts +0 -20
  55. package/src/session/__tests__/hubSync.test.ts +0 -51
  56. package/src/session/hubSync.ts +0 -79
package/dist/cjs/index.js CHANGED
@@ -20,9 +20,9 @@
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.DeviceManager = exports.deriveSharedSecret = exports.AEAD_NONCE_LENGTH = exports.AEAD_KEY_LENGTH = exports.decryptAead = exports.encryptAead = exports.hkdfSha256 = exports.RecoveryPhraseService = exports.SignatureService = exports.updateIdentityMarker = exports.readIdentityMarker = exports.IdentityUnavailableError = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.verifyPublicCardAttestation = exports.parseAttestPayload = exports.parseIdPayload = exports.buildUserDid = exports.ORGANIZATION_CATEGORIES = exports.normalizeProfileLinks = exports.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.OxyAppDataIdentifierError = exports.commonsDeliveryPlatform = exports.pushTargetsFromDelivery = exports.selectCommonsDelivery = exports.parseCommonsApprovalExpiresAt = exports.getCommonsApprovalBlockingReason = exports.ServiceCredentialMismatchError = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.AssetUrlResolutionError = exports.OxyServices = void 0;
22
22
  exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isWebBrowser = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.getPrimaryLanguage = exports.getUserLanguages = exports.isRTLLocale = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.isSupportedLocale = exports.normalizeLocale = exports.getBaseLanguage = exports.FALLBACK_LOCALE = exports.SUPPORTED_LANGUAGES = exports.TopicSource = void 0;
23
- exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = exports.normalizeMultilineText = exports.normalizeInlineText = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.DISPLAY_NAME_ORPHANED_MARK_SOURCE = exports.DISPLAY_NAME_DISALLOWED_SOURCE = exports.DISPLAY_NAME_ALLOWED_SCRIPTS = exports.isValidDisplayName = exports.isValidPassword = exports.isValidUsername = exports.isValidEmail = exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.translate = void 0;
24
- exports.createNativeIdentityPinStore = exports.createWebIdentityPinStore = exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = exports.AccountDialogController = exports.switchableAccountIds = exports.projectSwitchableAccounts = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.redeemHubTicketOnHub = exports.syncHubAfterSignIn = exports.parseHubSyncReturnUrl = exports.normalizeOfficialReturnOrigin = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.isIdpHubOrigin = exports.buildHubSyncUrl = exports.buildIdpHubOrigin = exports.consumeOAuthReturnPath = exports.persistOAuthReturnPath = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.canonicalizeOAuthRedirectUri = exports.normalizeOAuthRedirectUri = exports.OXY_OAUTH_RETURN_PATH_STORAGE_KEY = exports.OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY = exports.OXY_SILENT_OAUTH_ATTEMPTED_KEY = exports.OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.runColdBoot = exports.isOxyRpOrigin = exports.CENTRAL_IDP_APEX = exports.registrableApex = void 0;
25
- exports.packageInfo = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshDeviceSecretArm = exports.refreshPersistedSession = exports.AccountNotOnDeviceError = exports.establishIdentitySession = exports.resolveIdentityPin = exports.IDENTITY_PIN_STORAGE_KEY = exports.identityPinMatches = exports.createMemoryIdentityPinStore = void 0;
23
+ exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = exports.normalizeMultilineText = exports.normalizeInlineText = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.DISPLAY_NAME_ORPHANED_MARK_SOURCE = exports.DISPLAY_NAME_DISALLOWED_SOURCE = exports.DISPLAY_NAME_ALLOWED_SCRIPTS = exports.isValidDisplayName = exports.isValidPassword = exports.isValidUsername = exports.isValidEmail = exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.buildQueryParams = exports.translate = void 0;
24
+ exports.refreshDeviceSecretArm = exports.refreshPersistedSession = exports.AccountNotOnDeviceError = exports.establishIdentitySession = exports.resolveIdentityPin = exports.IDENTITY_PIN_STORAGE_KEY = exports.identityPinMatches = exports.createMemoryIdentityPinStore = exports.createNativeIdentityPinStore = exports.createWebIdentityPinStore = exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = exports.AccountDialogController = exports.switchableAccountIds = exports.projectSwitchableAccounts = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.consumeOAuthReturnPath = exports.persistOAuthReturnPath = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.canonicalizeOAuthRedirectUri = exports.normalizeOAuthRedirectUri = exports.OXY_OAUTH_RETURN_PATH_STORAGE_KEY = exports.OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.runColdBoot = exports.isOxyRpOrigin = exports.CENTRAL_IDP_APEX = exports.registrableApex = exports.getAccountColor = void 0;
25
+ exports.packageInfo = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = void 0;
26
26
  // Ensure crypto polyfills are loaded before anything else
27
27
  require("./crypto/polyfill");
28
28
  // ---------------------------------------------------------------------------
@@ -215,6 +215,7 @@ Object.defineProperty(exports, "translate", { enumerable: true, get: function ()
215
215
  // API request / URL helpers
216
216
  // ---------------------------------------------------------------------------
217
217
  var apiUtils_1 = require("./utils/apiUtils");
218
+ Object.defineProperty(exports, "buildQueryParams", { enumerable: true, get: function () { return apiUtils_1.buildQueryParams; } });
218
219
  Object.defineProperty(exports, "buildSearchParams", { enumerable: true, get: function () { return apiUtils_1.buildSearchParams; } });
219
220
  Object.defineProperty(exports, "buildUrl", { enumerable: true, get: function () { return apiUtils_1.buildUrl; } });
220
221
  Object.defineProperty(exports, "buildPaginationParams", { enumerable: true, get: function () { return apiUtils_1.buildPaginationParams; } });
@@ -319,8 +320,6 @@ Object.defineProperty(exports, "OXY_AUTHORIZE_URL", { enumerable: true, get: fun
319
320
  Object.defineProperty(exports, "OXY_OAUTH_STATE_STORAGE_KEY", { enumerable: true, get: function () { return oauthPkce_1.OXY_OAUTH_STATE_STORAGE_KEY; } });
320
321
  Object.defineProperty(exports, "OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY", { enumerable: true, get: function () { return oauthPkce_1.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY; } });
321
322
  Object.defineProperty(exports, "OXY_OAUTH_REDIRECT_URI_STORAGE_KEY", { enumerable: true, get: function () { return oauthPkce_1.OXY_OAUTH_REDIRECT_URI_STORAGE_KEY; } });
322
- Object.defineProperty(exports, "OXY_SILENT_OAUTH_ATTEMPTED_KEY", { enumerable: true, get: function () { return oauthPkce_1.OXY_SILENT_OAUTH_ATTEMPTED_KEY; } });
323
- Object.defineProperty(exports, "OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY", { enumerable: true, get: function () { return oauthPkce_1.OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY; } });
324
323
  Object.defineProperty(exports, "OXY_OAUTH_RETURN_PATH_STORAGE_KEY", { enumerable: true, get: function () { return oauthPkce_1.OXY_OAUTH_RETURN_PATH_STORAGE_KEY; } });
325
324
  Object.defineProperty(exports, "normalizeOAuthRedirectUri", { enumerable: true, get: function () { return oauthPkce_1.normalizeOAuthRedirectUri; } });
326
325
  Object.defineProperty(exports, "canonicalizeOAuthRedirectUri", { enumerable: true, get: function () { return oauthPkce_1.canonicalizeOAuthRedirectUri; } });
@@ -330,17 +329,9 @@ Object.defineProperty(exports, "clearOAuthHandshake", { enumerable: true, get: f
330
329
  Object.defineProperty(exports, "persistOAuthReturnPath", { enumerable: true, get: function () { return oauthPkce_1.persistOAuthReturnPath; } });
331
330
  Object.defineProperty(exports, "consumeOAuthReturnPath", { enumerable: true, get: function () { return oauthPkce_1.consumeOAuthReturnPath; } });
332
331
  var officialOrigins_1 = require("./utils/officialOrigins");
333
- Object.defineProperty(exports, "buildIdpHubOrigin", { enumerable: true, get: function () { return officialOrigins_1.buildIdpHubOrigin; } });
334
- Object.defineProperty(exports, "buildHubSyncUrl", { enumerable: true, get: function () { return officialOrigins_1.buildHubSyncUrl; } });
335
- Object.defineProperty(exports, "isIdpHubOrigin", { enumerable: true, get: function () { return officialOrigins_1.isIdpHubOrigin; } });
336
332
  Object.defineProperty(exports, "isLoopbackOrigin", { enumerable: true, get: function () { return officialOrigins_1.isLoopbackOrigin; } });
337
333
  Object.defineProperty(exports, "isOfficialWebOrigin", { enumerable: true, get: function () { return officialOrigins_1.isOfficialWebOrigin; } });
338
334
  Object.defineProperty(exports, "isAllowedDeviceJoinOrigin", { enumerable: true, get: function () { return officialOrigins_1.isAllowedDeviceJoinOrigin; } });
339
- Object.defineProperty(exports, "normalizeOfficialReturnOrigin", { enumerable: true, get: function () { return officialOrigins_1.normalizeOfficialReturnOrigin; } });
340
- Object.defineProperty(exports, "parseHubSyncReturnUrl", { enumerable: true, get: function () { return officialOrigins_1.parseHubSyncReturnUrl; } });
341
- var hubSync_1 = require("./session/hubSync");
342
- Object.defineProperty(exports, "syncHubAfterSignIn", { enumerable: true, get: function () { return hubSync_1.syncHubAfterSignIn; } });
343
- Object.defineProperty(exports, "redeemHubTicketOnHub", { enumerable: true, get: function () { return hubSync_1.redeemHubTicketOnHub; } });
344
335
  // ---------------------------------------------------------------------------
345
336
  // Session sync (device-scoped multi-account session client)
346
337
  // ---------------------------------------------------------------------------
@@ -110,36 +110,5 @@ function OxyServicesDeviceBootMixin(Base) {
110
110
  throw normalized;
111
111
  }
112
112
  }
113
- /** Mint a one-time hub sync ticket (bearer required). */
114
- async issueHubTicket(returnOrigin) {
115
- try {
116
- const res = await this.makeRequest('POST', '/session/device/hub-ticket', { returnOrigin }, { cache: false });
117
- const parsed = (0, contracts_1.safeParseContract)(contracts_1.deviceHubTicketIssueResponseSchema, res);
118
- if (!parsed) {
119
- throw new Error('session/device/hub-ticket returned an unexpected response shape');
120
- }
121
- return parsed;
122
- }
123
- catch (error) {
124
- throw this.handleError(error);
125
- }
126
- }
127
- /** Redeem a hub sync ticket for a fresh device secret (public). */
128
- async redeemHubTicket(ticket, returnOrigin) {
129
- try {
130
- const res = await this.makeRequest('POST', '/session/device/redeem-ticket', { ticket, returnOrigin },
131
- // Public device-hub sync mint (bearer-less). Same control-plane class as
132
- // the device-secret mint — bypassQueue so it never waits for a slot.
133
- { cache: false, skipAuth: true, bypassQueue: true });
134
- const parsed = (0, contracts_1.safeParseContract)(contracts_1.deviceHubTicketRedeemResponseSchema, res);
135
- if (!parsed) {
136
- throw new Error('session/device/redeem-ticket returned an unexpected response shape');
137
- }
138
- return parsed;
139
- }
140
- catch (error) {
141
- throw this.handleError(error);
142
- }
143
- }
144
113
  };
145
114
  }
@@ -68,10 +68,7 @@ function OxyServicesUserMixin(Base) {
68
68
  */
69
69
  async searchProfiles(query, pagination) {
70
70
  try {
71
- const params = { query, ...pagination };
72
- const searchParams = (0, apiUtils_1.buildSearchParams)(params);
73
- const paramsObj = Object.fromEntries(searchParams.entries());
74
- const response = await this.makeRequest('GET', '/profiles/search', paramsObj, {
71
+ const response = await this.makeRequest('GET', '/profiles/search', (0, apiUtils_1.buildQueryParams)({ query, ...pagination }), {
75
72
  cache: true,
76
73
  cacheTTL: 2 * 60 * 1000, // 2 minutes cache
77
74
  });
@@ -529,6 +526,41 @@ function OxyServicesUserMixin(Base) {
529
526
  throw this.handleError(error);
530
527
  }
531
528
  }
529
+ /**
530
+ * Invalidate every cached read a follow/unfollow write invalidates.
531
+ *
532
+ * Shared by the four mutation entry points (`followUser`, `unfollowUser`,
533
+ * `followUsers`, `unfollowUsers`) so they can never drift on which caches a
534
+ * write busts.
535
+ *
536
+ * The follower/following/mutuals LISTS are cleared by PREFIX rather than by
537
+ * exact key. Those reads are paginated and ordered, so one logical list is
538
+ * spread across many content-addressed keys
539
+ * (`GET:/users/<id>/followers:{"limit":"20","offset":"40","sort":"oldest"}`);
540
+ * an exact-key clear would only bust whichever page/sort variant happened to
541
+ * be read last and would leave every other page stale. `clearCacheByPrefix`
542
+ * deletes all of them, and all identity-scoped variants of each.
543
+ */
544
+ invalidateFollowGraphCaches(targetUserIds) {
545
+ for (const id of targetUserIds) {
546
+ this.clearCacheEntry(`GET:/users/${id}/follow-status`);
547
+ // Profile fetches embed viewer-relative `relationship` — bust so a
548
+ // remount doesn't serve a stale isFollowing for up to 5 minutes.
549
+ this.clearCacheEntry(`GET:/users/${id}`);
550
+ // The target gained/lost a follower, and the viewer's presence in the
551
+ // target's "followers you know" set changed with it.
552
+ this.clearCacheByPrefix(`GET:/users/${id}/followers`);
553
+ this.clearCacheByPrefix(`GET:/users/${id}/mutuals`);
554
+ }
555
+ this.clearCacheByPrefix('GET:/profiles/username/');
556
+ this.clearCacheByPrefix('GET:/profiles/resolve');
557
+ // The write changed the viewer's OWN following list and graph.
558
+ const viewerId = this.getCurrentUserId();
559
+ if (viewerId) {
560
+ this.clearCacheByPrefix(`GET:/users/${viewerId}/following`);
561
+ }
562
+ this.clearCacheEntry('GET:/users/me/graph');
563
+ }
532
564
  /**
533
565
  * Follow a user.
534
566
  *
@@ -542,16 +574,7 @@ function OxyServicesUserMixin(Base) {
542
574
  async followUser(userId) {
543
575
  try {
544
576
  const result = await this.makeRequest('POST', `/users/${userId}/follow`, undefined, { cache: false });
545
- this.clearCacheEntry(`GET:/users/${userId}/follow-status`);
546
- // Profile fetches embed viewer-relative `relationship` — bust so a
547
- // remount doesn't serve a stale isFollowing for up to 5 minutes.
548
- this.clearCacheEntry(`GET:/users/${userId}`);
549
- this.clearCacheByPrefix('GET:/profiles/username/');
550
- this.clearCacheByPrefix('GET:/profiles/resolve');
551
- // The follow changed the viewer's graph — bust the cached consolidated
552
- // `GET /users/me/graph` so the next read reflects the new following/
553
- // mutual set instead of the stale pre-write snapshot.
554
- this.clearCacheEntry('GET:/users/me/graph');
577
+ this.invalidateFollowGraphCaches([userId]);
555
578
  return result;
556
579
  }
557
580
  catch (error) {
@@ -572,15 +595,7 @@ function OxyServicesUserMixin(Base) {
572
595
  }
573
596
  try {
574
597
  const result = await this.makeRequest('POST', '/users/follow/bulk', { userIds }, { cache: false });
575
- // Bust each affected user's cached follow-status (see `followUser`).
576
- for (const id of userIds) {
577
- this.clearCacheEntry(`GET:/users/${id}/follow-status`);
578
- this.clearCacheEntry(`GET:/users/${id}`);
579
- }
580
- this.clearCacheByPrefix('GET:/profiles/username/');
581
- this.clearCacheByPrefix('GET:/profiles/resolve');
582
- // The batch changed the viewer's graph — bust the consolidated cache.
583
- this.clearCacheEntry('GET:/users/me/graph');
598
+ this.invalidateFollowGraphCaches(userIds);
584
599
  return result;
585
600
  }
586
601
  catch (error) {
@@ -601,15 +616,7 @@ function OxyServicesUserMixin(Base) {
601
616
  }
602
617
  try {
603
618
  const result = await this.makeRequest('POST', '/users/unfollow/bulk', { userIds }, { cache: false });
604
- // Bust each affected user's cached follow-status (see `followUser`).
605
- for (const id of userIds) {
606
- this.clearCacheEntry(`GET:/users/${id}/follow-status`);
607
- this.clearCacheEntry(`GET:/users/${id}`);
608
- }
609
- this.clearCacheByPrefix('GET:/profiles/username/');
610
- this.clearCacheByPrefix('GET:/profiles/resolve');
611
- // The batch changed the viewer's graph — bust the consolidated cache.
612
- this.clearCacheEntry('GET:/users/me/graph');
619
+ this.invalidateFollowGraphCaches(userIds);
613
620
  return result;
614
621
  }
615
622
  catch (error) {
@@ -622,13 +629,7 @@ function OxyServicesUserMixin(Base) {
622
629
  async unfollowUser(userId) {
623
630
  try {
624
631
  const result = await this.makeRequest('DELETE', `/users/${userId}/follow`, undefined, { cache: false });
625
- // Bust the cached follow-status so a remount reads fresh truth (see `followUser`).
626
- this.clearCacheEntry(`GET:/users/${userId}/follow-status`);
627
- this.clearCacheEntry(`GET:/users/${userId}`);
628
- this.clearCacheByPrefix('GET:/profiles/username/');
629
- this.clearCacheByPrefix('GET:/profiles/resolve');
630
- // The unfollow changed the viewer's graph — bust the consolidated cache.
631
- this.clearCacheEntry('GET:/users/me/graph');
632
+ this.invalidateFollowGraphCaches([userId]);
632
633
  return result;
633
634
  }
634
635
  catch (error) {
@@ -687,11 +688,16 @@ function OxyServicesUserMixin(Base) {
687
688
  }
688
689
  }
689
690
  /**
690
- * Get user followers
691
+ * Get user followers.
692
+ *
693
+ * `sort` orders the underlying follow edges — `recent` (newest first, the
694
+ * server default) or `oldest`. Because the response is cached and the cache
695
+ * key is content-addressed on the query params, each `limit`/`offset`/`sort`
696
+ * combination is its own entry.
691
697
  */
692
698
  async getUserFollowers(userId, pagination) {
693
699
  try {
694
- const params = (0, apiUtils_1.buildPaginationParams)(pagination || {});
700
+ const params = (0, apiUtils_1.buildQueryParams)(pagination || {});
695
701
  const response = await this.makeRequest('GET', `/users/${userId}/followers`, params, {
696
702
  cache: true,
697
703
  cacheTTL: 2 * 60 * 1000, // 2 minutes cache
@@ -707,11 +713,11 @@ function OxyServicesUserMixin(Base) {
707
713
  }
708
714
  }
709
715
  /**
710
- * Get user following
716
+ * Get user following. `sort` behaves as in {@link getUserFollowers}.
711
717
  */
712
718
  async getUserFollowing(userId, pagination) {
713
719
  try {
714
- const params = (0, apiUtils_1.buildPaginationParams)(pagination || {});
720
+ const params = (0, apiUtils_1.buildQueryParams)(pagination || {});
715
721
  const response = await this.makeRequest('GET', `/users/${userId}/following`, params, {
716
722
  cache: true,
717
723
  cacheTTL: 2 * 60 * 1000, // 2 minutes cache
@@ -732,7 +738,7 @@ function OxyServicesUserMixin(Base) {
732
738
  */
733
739
  async getUserMutuals(userId, pagination) {
734
740
  try {
735
- const params = (0, apiUtils_1.buildPaginationParams)(pagination || {});
741
+ const params = (0, apiUtils_1.buildQueryParams)(pagination || {});
736
742
  const response = await this.makeRequest('GET', `/users/${userId}/mutuals`, params, {
737
743
  cache: true,
738
744
  cacheTTL: 2 * 60 * 1000, // 2 minutes cache
@@ -16,7 +16,7 @@
16
16
  * ```
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.parseHubSyncReturnUrl = exports.normalizeOfficialReturnOrigin = exports.isOfficialWebOrigin = exports.isIdpHubOrigin = exports.buildHubSyncUrl = exports.buildIdpHubOrigin = exports.registrableApex = exports.verifySecret = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.OXY_SERVICE_ENVIRONMENTS = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getOxyUserId = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = void 0;
19
+ exports.isOfficialWebOrigin = exports.registrableApex = exports.verifySecret = exports.OXY_CSP_BASELINE = exports.formatOxyCspPolicy = exports.createOxySecurityHeaders = exports.buildOxyPagesHeaders = exports.buildOxyCspDirectives = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.OXY_SERVICE_ENVIRONMENTS = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getOxyUserId = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = void 0;
20
20
  var auth_1 = require("./auth");
21
21
  Object.defineProperty(exports, "createOptionalOxyAuth", { enumerable: true, get: function () { return auth_1.createOptionalOxyAuth; } });
22
22
  Object.defineProperty(exports, "createOxyAuthMiddleware", { enumerable: true, get: function () { return auth_1.createOxyAuthMiddleware; } });
@@ -44,6 +44,14 @@ Object.defineProperty(exports, "UPSTREAM_HEADERS_TIMEOUT_MS", { enumerable: true
44
44
  // Strict CORS allowlist (Oxy apex family + explicit app origins).
45
45
  var cors_1 = require("./cors");
46
46
  Object.defineProperty(exports, "createOxyCors", { enumerable: true, get: function () { return cors_1.createOxyCors; } });
47
+ // Shared Helmet + Content-Security-Policy baseline (Cloudflare Insights beacon,
48
+ // Oxy API/CDN origins) with additive, per-app extensions.
49
+ var securityHeaders_1 = require("./securityHeaders");
50
+ Object.defineProperty(exports, "buildOxyCspDirectives", { enumerable: true, get: function () { return securityHeaders_1.buildOxyCspDirectives; } });
51
+ Object.defineProperty(exports, "buildOxyPagesHeaders", { enumerable: true, get: function () { return securityHeaders_1.buildOxyPagesHeaders; } });
52
+ Object.defineProperty(exports, "createOxySecurityHeaders", { enumerable: true, get: function () { return securityHeaders_1.createOxySecurityHeaders; } });
53
+ Object.defineProperty(exports, "formatOxyCspPolicy", { enumerable: true, get: function () { return securityHeaders_1.formatOxyCspPolicy; } });
54
+ Object.defineProperty(exports, "OXY_CSP_BASELINE", { enumerable: true, get: function () { return securityHeaders_1.OXY_CSP_BASELINE; } });
47
55
  // Constant-time secret comparison.
48
56
  var verifySecret_1 = require("./verifySecret");
49
57
  Object.defineProperty(exports, "verifySecret", { enumerable: true, get: function () { return verifySecret_1.verifySecret; } });
@@ -54,9 +62,4 @@ Object.defineProperty(exports, "verifySecret", { enumerable: true, get: function
54
62
  var registrableApex_1 = require("../utils/registrableApex");
55
63
  Object.defineProperty(exports, "registrableApex", { enumerable: true, get: function () { return registrableApex_1.registrableApex; } });
56
64
  var officialOrigins_1 = require("../utils/officialOrigins");
57
- Object.defineProperty(exports, "buildIdpHubOrigin", { enumerable: true, get: function () { return officialOrigins_1.buildIdpHubOrigin; } });
58
- Object.defineProperty(exports, "buildHubSyncUrl", { enumerable: true, get: function () { return officialOrigins_1.buildHubSyncUrl; } });
59
- Object.defineProperty(exports, "isIdpHubOrigin", { enumerable: true, get: function () { return officialOrigins_1.isIdpHubOrigin; } });
60
65
  Object.defineProperty(exports, "isOfficialWebOrigin", { enumerable: true, get: function () { return officialOrigins_1.isOfficialWebOrigin; } });
61
- Object.defineProperty(exports, "normalizeOfficialReturnOrigin", { enumerable: true, get: function () { return officialOrigins_1.normalizeOfficialReturnOrigin; } });
62
- Object.defineProperty(exports, "parseHubSyncReturnUrl", { enumerable: true, get: function () { return officialOrigins_1.parseHubSyncReturnUrl; } });
@@ -174,6 +174,9 @@ function createOxyRateLimit(oxy, options = {}) {
174
174
  standardHeaders: true,
175
175
  legacyHeaders: false,
176
176
  skip,
177
+ // hashAnonymousIp already buckets IPv6 to /56 before HMAC; disable the v8
178
+ // static source scan that false-positives on req.ip (ERR_ERL_KEY_GEN_IPV6).
179
+ validate: { keyGeneratorIpFallback: false },
177
180
  });
178
181
  return (req, res, next) => {
179
182
  // Skipped paths bypass BOTH session resolution and limiting — cheap and
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ /**
3
+ * Shared security headers (Helmet + Content-Security-Policy) for Oxy backends.
4
+ *
5
+ * WHY THIS EXISTS
6
+ * ---------------
7
+ * A CSP only governs an origin that serves DOCUMENTS; on a JSON API it governs
8
+ * no browsing context. The Oxy origins that serve HTML through Cloudflare have
9
+ * so far either hand-written their own policy or shipped none at all, and two
10
+ * bugs follow from that:
11
+ *
12
+ * 1. THE CLOUDFLARE INSIGHTS BEACON IS BLOCKED BY A HAND-WRITTEN POLICY.
13
+ * Cloudflare injects `<script src="https://static.cloudflareinsights.com/
14
+ * beacon.min.js/...">` into HTML it proxies. No application code loads it,
15
+ * so it cannot be allowlisted from the app side any other way, and an
16
+ * origin whose policy says `script-src 'self'` logs
17
+ * `Loading the script 'https://static.cloudflareinsights.com/beacon.min.js'
18
+ * violates the following Content Security Policy directive: "script-src
19
+ * 'self'"` and collects nothing. The beacon needs BOTH hosts, and they are
20
+ * different halves of the same feature: `static.cloudflareinsights.com`
21
+ * serves the script (`script-src`), `cloudflareinsights.com` receives the
22
+ * measurements (`connect-src`). Allowing only the script leaves the beacon
23
+ * loading but unable to report, which looks fixed and is not. Verified in
24
+ * production 2026-07-29: `mention.earth` serves HTML behind Cloudflare with
25
+ * the beacon injected and `script-src 'self'` — blocked; `oxy.so` had
26
+ * already allowlisted the same two hosts in its own static `_headers`,
27
+ * independently, which is the divergence this baseline exists to end.
28
+ *
29
+ * 2. AN EXPLICIT DIRECTIVE SILENTLY REPLACES HELMET'S DEFAULT.
30
+ * Writing `scriptSrc: ['https://example.com']` drops `'self'` — the page's
31
+ * own bundle stops loading (or, worse, only some lazily-loaded chunk does,
32
+ * so it ships). This helper makes that structurally impossible: callers can
33
+ * only ADD sources to the Oxy baseline, never replace a directive, and they
34
+ * cannot pass their own `contentSecurityPolicy` through to Helmet at all
35
+ * (the option is typed `never`).
36
+ *
37
+ * WHAT IT PROVIDES
38
+ * ----------------
39
+ * `createOxySecurityHeaders(options)` returns the Helmet middleware with the
40
+ * Oxy-wide CSP baseline applied, plus per-app extensions merged (and deduped)
41
+ * into it. Everything Helmet does that is NOT the CSP (HSTS, frameguard,
42
+ * referrer policy, CORP/COOP, …) is passed straight through, so an app keeps
43
+ * full control of those.
44
+ *
45
+ * `buildOxyCspDirectives(extensions)` is the same resolution as a pure
46
+ * function, for the Oxy document origins that are NOT Express — a Next.js
47
+ * `headers()`, a Cloudflare Pages `_headers` generator — so one policy can
48
+ * cover them without a second implementation.
49
+ *
50
+ * SCOPE: mount this on backends that serve HTML. A JSON-only API gains nothing
51
+ * from a source-list CSP; harden those with the non-CSP headers instead
52
+ * (`hsts`, `noSniff`, `frameguard`, CORP) rather than adding directives that
53
+ * apply to no document.
54
+ *
55
+ * Node/Express-only: exported solely from `@oxyhq/core/server`.
56
+ */
57
+ var __importDefault = (this && this.__importDefault) || function (mod) {
58
+ return (mod && mod.__esModule) ? mod : { "default": mod };
59
+ };
60
+ Object.defineProperty(exports, "__esModule", { value: true });
61
+ exports.OXY_CSP_BASELINE = void 0;
62
+ exports.buildOxyCspDirectives = buildOxyCspDirectives;
63
+ exports.formatOxyCspPolicy = formatOxyCspPolicy;
64
+ exports.buildOxyPagesHeaders = buildOxyPagesHeaders;
65
+ exports.createOxySecurityHeaders = createOxySecurityHeaders;
66
+ const helmet_1 = __importDefault(require("helmet"));
67
+ /** CSP keyword for "this origin". Always present in every open baseline directive. */
68
+ const SELF = "'self'";
69
+ /** CSP keyword for a fully closed directive. Meaningless alongside any other source. */
70
+ const NONE = "'none'";
71
+ /**
72
+ * Cloudflare Web Analytics. Injected at the edge into proxied HTML — no Oxy app
73
+ * loads it, and no Oxy app should have to know these hostnames. Both are
74
+ * required: the script host, and the host the beacon reports to.
75
+ */
76
+ const CLOUDFLARE_INSIGHTS_SCRIPT_ORIGIN = 'https://static.cloudflareinsights.com';
77
+ const CLOUDFLARE_INSIGHTS_REPORT_ORIGIN = 'https://cloudflareinsights.com';
78
+ /**
79
+ * Oxy platform origins. Every Oxy web origin runs the SDK, which calls the Oxy
80
+ * API over HTTPS and Socket.IO, and resolves all canonical media through the
81
+ * Oxy CDN (`getFileDownloadUrl` → `cloud.oxy.so`).
82
+ */
83
+ const OXY_API_ORIGIN = 'https://api.oxy.so';
84
+ const OXY_API_WEBSOCKET_ORIGIN = 'wss://api.oxy.so';
85
+ const OXY_CDN_ORIGIN = 'https://cloud.oxy.so';
86
+ /**
87
+ * The Oxy-wide CSP baseline. Deliberately the floor every Oxy origin needs, not
88
+ * a superset of what any one app allows — permissive sources an individual app
89
+ * wants (`https:` images, `blob:` media, embed hosts, LiveKit) are that app's
90
+ * extension, so each widening stays visible at its call site.
91
+ *
92
+ * `style-src` carries `'unsafe-inline'` because react-native-web injects its
93
+ * stylesheet as inline `<style>` at runtime; without it every Oxy web app
94
+ * renders unstyled.
95
+ */
96
+ exports.OXY_CSP_BASELINE = Object.freeze({
97
+ defaultSrc: Object.freeze([SELF]),
98
+ baseUri: Object.freeze([SELF]),
99
+ formAction: Object.freeze([SELF]),
100
+ frameAncestors: Object.freeze([NONE]),
101
+ objectSrc: Object.freeze([NONE]),
102
+ scriptSrc: Object.freeze([SELF, CLOUDFLARE_INSIGHTS_SCRIPT_ORIGIN]),
103
+ scriptSrcAttr: Object.freeze([NONE]),
104
+ styleSrc: Object.freeze([SELF, "'unsafe-inline'"]),
105
+ imgSrc: Object.freeze([SELF, 'data:', OXY_CDN_ORIGIN]),
106
+ mediaSrc: Object.freeze([SELF, OXY_CDN_ORIGIN]),
107
+ fontSrc: Object.freeze([SELF, 'data:']),
108
+ connectSrc: Object.freeze([
109
+ SELF,
110
+ CLOUDFLARE_INSIGHTS_REPORT_ORIGIN,
111
+ OXY_API_ORIGIN,
112
+ OXY_API_WEBSOCKET_ORIGIN,
113
+ OXY_CDN_ORIGIN,
114
+ ]),
115
+ });
116
+ /** `connectSrc` → `connect-src`. Total over `OxyCspDirective` (all are camelCase ASCII). */
117
+ function toHeaderDirectiveName(directive) {
118
+ return directive.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
119
+ }
120
+ /**
121
+ * A source containing `;` or `,` would silently terminate the directive (or the
122
+ * whole policy) and hand the rest of the string to the browser as new
123
+ * directives. Helmet rejects these too; we reject them here so the pure builder
124
+ * is equally safe, and so the failure names the offending directive.
125
+ */
126
+ function assertValidSource(directive, source) {
127
+ if (typeof source !== 'string' || source.length === 0) {
128
+ throw new TypeError(`Oxy CSP: ${toHeaderDirectiveName(directive)} received an empty source.`);
129
+ }
130
+ if (source.includes(';') || source.includes(',')) {
131
+ throw new TypeError(`Oxy CSP: ${toHeaderDirectiveName(directive)} source ${JSON.stringify(source)} may not contain ";" or ",".`);
132
+ }
133
+ }
134
+ /** Order-preserving, first-seen-wins dedupe. */
135
+ function dedupe(sources) {
136
+ return [...new Set(sources)];
137
+ }
138
+ /**
139
+ * Resolve the effective CSP directives: the Oxy baseline, with each app
140
+ * extension merged in and deduped.
141
+ *
142
+ * Merge rules:
143
+ * - A baseline directive is EXTENDED, never replaced — `'self'` and the
144
+ * Cloudflare beacon hosts always survive.
145
+ * - A directive absent from the baseline is seeded with `'self'`, so adding
146
+ * (say) an embed host to `frame-src` cannot lock the origin out of itself.
147
+ * - A directive whose baseline is exactly `'none'` is CLOSED: extending it
148
+ * drops the sentinel, because `'none'` alongside any other source is
149
+ * meaningless per the CSP spec. This is how an app that must be framable
150
+ * opts back in with `frameAncestors: ["'self'"]`.
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * buildOxyCspDirectives({ frameSrc: ['https://player.vimeo.com'] });
155
+ * // → { ..., 'frame-src': ["'self'", 'https://player.vimeo.com'], ... }
156
+ * ```
157
+ */
158
+ function buildOxyCspDirectives(extensions = {}) {
159
+ const directiveNames = new Set([
160
+ ...Object.keys(exports.OXY_CSP_BASELINE),
161
+ ...Object.keys(extensions),
162
+ ]);
163
+ const resolved = {};
164
+ for (const directive of directiveNames) {
165
+ const extras = extensions[directive] ?? [];
166
+ for (const source of extras) {
167
+ assertValidSource(directive, source);
168
+ }
169
+ const baseline = exports.OXY_CSP_BASELINE[directive] ?? [SELF];
170
+ const isClosed = baseline.length === 1 && baseline[0] === NONE;
171
+ const merged = isClosed && extras.length > 0 ? extras : [...baseline, ...extras];
172
+ resolved[toHeaderDirectiveName(directive)] = dedupe(merged);
173
+ }
174
+ // Valueless directive: rewrite stray `http://` subresources to HTTPS rather
175
+ // than failing them, which matters for federated/user-supplied URLs.
176
+ resolved['upgrade-insecure-requests'] = [];
177
+ return resolved;
178
+ }
179
+ /**
180
+ * Serialize resolved CSP directives into the single-line header value browsers
181
+ * and Cloudflare `_headers` expect. Valueless directives (e.g.
182
+ * `upgrade-insecure-requests`) emit the name alone.
183
+ */
184
+ function formatOxyCspPolicy(directives) {
185
+ return Object.entries(directives)
186
+ .map(([name, sources]) => (sources.length === 0 ? name : `${name} ${sources.join(' ')}`))
187
+ .join('; ');
188
+ }
189
+ /**
190
+ * Build a Cloudflare Pages `_headers` block for an Oxy HTML origin. Uses the
191
+ * same CSP resolution as {@link createOxySecurityHeaders} plus the non-CSP
192
+ * hardening headers Helmet would add on an Express HTML backend.
193
+ */
194
+ function buildOxyPagesHeaders(options = {}) {
195
+ const csp = formatOxyCspPolicy(buildOxyCspDirectives(options.csp));
196
+ const lines = [
197
+ '/*',
198
+ ` Content-Security-Policy: ${csp}`,
199
+ ' X-Frame-Options: DENY',
200
+ ' X-Content-Type-Options: nosniff',
201
+ ' Referrer-Policy: strict-origin-when-cross-origin',
202
+ ];
203
+ if (options.hsts !== false) {
204
+ lines.push(' Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
205
+ }
206
+ lines.push('');
207
+ return lines.join('\n');
208
+ }
209
+ /**
210
+ * Build the shared Oxy security-headers middleware: Helmet with the Oxy CSP
211
+ * baseline plus this app's extensions.
212
+ *
213
+ * @example
214
+ * ```ts
215
+ * app.use(createOxySecurityHeaders({
216
+ * csp: {
217
+ * connectSrc: ['https://api.example.com', 'wss://api.example.com'],
218
+ * frameSrc: ['https://player.vimeo.com'],
219
+ * },
220
+ * helmet: { crossOriginResourcePolicy: { policy: 'cross-origin' } },
221
+ * }));
222
+ * ```
223
+ */
224
+ function createOxySecurityHeaders(options = {}) {
225
+ const { csp, helmet: helmetOptions } = options;
226
+ const directives = buildOxyCspDirectives(csp);
227
+ return (0, helmet_1.default)({
228
+ ...helmetOptions,
229
+ // `useDefaults: false`: the baseline above is the whole policy, so what the
230
+ // browser receives is exactly what `buildOxyCspDirectives` returns — no
231
+ // silent union with Helmet's defaults that tests would never see.
232
+ contentSecurityPolicy: { useDefaults: false, directives },
233
+ });
234
+ }
@@ -481,9 +481,9 @@ class AccountDialogController {
481
481
  user: result.user,
482
482
  accessToken: result.accessToken,
483
483
  }, result.user,
484
- // A switch is IN-PLACE: commit without the hub-sync redirect (the
485
- // device is already known/synced). Cross-tab/app propagation rides the
486
- // server's `session_state` socket broadcast, not a navigation.
484
+ // A switch is IN-PLACE: use the switch commit funnel (not sign-in).
485
+ // Cross-tab/app propagation rides the server's `session_state` socket
486
+ // broadcast, not a navigation.
487
487
  { fromSwitch: true });
488
488
  }
489
489
  // Re-project + refetch immediately; the subscription also fires.
@@ -986,11 +986,9 @@ class AccountDialogController {
986
986
  * consumer's commit funnel (durable persist + hydration); falls back to
987
987
  * `SessionClient.registerAndActivate` (registration + activation only).
988
988
  *
989
- * A SWITCH (`opts.fromSwitch`) uses the IN-PLACE `commitSwitchedSession` funnel
990
- * so it never runs the cross-origin hub-sync redirect; a SIGN-IN uses
991
- * `commitSession` (which may hub-sync on an official web origin). When the
992
- * switch funnel is not wired it falls back to the sign-in funnel, then to
993
- * `registerAndActivate`.
989
+ * A SWITCH (`opts.fromSwitch`) uses the IN-PLACE `commitSwitchedSession` funnel;
990
+ * a SIGN-IN uses `commitSession`. When the switch funnel is not wired it falls
991
+ * back to the sign-in funnel, then to `registerAndActivate`.
994
992
  */
995
993
  async commitAuthorizedSession(session, user, opts) {
996
994
  const commit = opts?.fromSwitch