@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.
- package/README.md +36 -2
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +4 -13
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +0 -31
- package/dist/cjs/mixins/OxyServices.user.js +50 -44
- package/dist/cjs/server/index.js +9 -6
- package/dist/cjs/server/rateLimit.js +3 -0
- package/dist/cjs/server/securityHeaders.js +234 -0
- package/dist/cjs/session/accountDialogController.js +6 -8
- package/dist/cjs/utils/apiUtils.js +40 -10
- package/dist/cjs/utils/oauthPkce.js +1 -5
- package/dist/cjs/utils/officialOrigins.js +3 -73
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +3 -4
- package/dist/esm/mixins/OxyServices.deviceBoot.js +1 -32
- package/dist/esm/mixins/OxyServices.user.js +51 -45
- package/dist/esm/server/index.js +4 -1
- package/dist/esm/server/rateLimit.js +3 -0
- package/dist/esm/server/securityHeaders.js +224 -0
- package/dist/esm/session/accountDialogController.js +6 -8
- package/dist/esm/utils/apiUtils.js +39 -10
- package/dist/esm/utils/oauthPkce.js +0 -4
- package/dist/esm/utils/officialOrigins.js +3 -68
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +5 -7
- package/dist/types/mixins/OxyServices.auth.d.ts +1 -12
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +1 -5
- package/dist/types/mixins/OxyServices.user.d.ts +27 -6
- package/dist/types/server/index.d.ts +3 -1
- package/dist/types/server/securityHeaders.d.ts +154 -0
- package/dist/types/session/accountDialogController.d.ts +9 -15
- package/dist/types/utils/apiUtils.d.ts +48 -6
- package/dist/types/utils/oauthPkce.d.ts +11 -7
- package/dist/types/utils/officialOrigins.d.ts +3 -13
- package/package.json +10 -5
- package/src/index.ts +9 -14
- package/src/mixins/OxyServices.auth.ts +6 -13
- package/src/mixins/OxyServices.deviceBoot.ts +0 -47
- package/src/mixins/OxyServices.user.ts +60 -49
- package/src/mixins/__tests__/commonsSignIn.test.ts +9 -2
- package/src/mixins/__tests__/followGraphPagination.test.ts +250 -0
- package/src/server/__tests__/securityHeaders.test.ts +244 -0
- package/src/server/index.ts +17 -8
- package/src/server/rateLimit.ts +3 -0
- package/src/server/securityHeaders.ts +304 -0
- package/src/session/__tests__/accountDialogController.test.ts +3 -5
- package/src/session/accountDialogController.ts +12 -18
- package/src/utils/__tests__/officialOrigins.test.ts +0 -57
- package/src/utils/apiUtils.ts +64 -15
- package/src/utils/oauthPkce.ts +11 -9
- package/src/utils/officialOrigins.ts +3 -70
- package/dist/cjs/session/hubSync.js +0 -55
- package/dist/esm/session/hubSync.js +0 -51
- package/dist/types/session/hubSync.d.ts +0 -20
- package/src/session/__tests__/hubSync.test.ts +0 -51
- 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.
|
|
24
|
-
exports.
|
|
25
|
-
exports.packageInfo = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler =
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
package/dist/cjs/server/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.
|
|
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:
|
|
485
|
-
//
|
|
486
|
-
//
|
|
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
|
-
*
|
|
991
|
-
*
|
|
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
|