@oxyhq/core 3.9.1 → 3.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/i18n/locales/en-US.json +9 -0
- package/dist/cjs/i18n/locales/es-ES.json +9 -0
- package/dist/cjs/i18n/locales/locales/en-US.json +9 -0
- package/dist/cjs/i18n/locales/locales/es-ES.json +9 -0
- package/dist/cjs/index.js +2 -3
- package/dist/cjs/mixins/OxyServices.assets.js +29 -6
- package/dist/cjs/mixins/OxyServices.utility.js +52 -23
- package/dist/cjs/server/cors.js +155 -0
- package/dist/cjs/server/index.js +21 -1
- package/dist/cjs/server/safeFetch.js +458 -0
- package/dist/cjs/server/verifySecret.js +50 -0
- package/dist/cjs/utils/fapiAutoDetect.js +12 -42
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/i18n/locales/en-US.json +9 -0
- package/dist/esm/i18n/locales/es-ES.json +9 -0
- package/dist/esm/i18n/locales/locales/en-US.json +9 -0
- package/dist/esm/i18n/locales/locales/es-ES.json +9 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/mixins/OxyServices.assets.js +29 -6
- package/dist/esm/mixins/OxyServices.utility.js +52 -23
- package/dist/esm/server/cors.js +152 -0
- package/dist/esm/server/index.js +6 -0
- package/dist/esm/server/safeFetch.js +447 -0
- package/dist/esm/server/verifySecret.js +47 -0
- package/dist/esm/utils/fapiAutoDetect.js +12 -41
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/mixins/OxyServices.assets.d.ts +6 -1
- package/dist/types/mixins/OxyServices.utility.d.ts +3 -3
- package/dist/types/server/cors.d.ts +57 -0
- package/dist/types/server/index.d.ts +5 -0
- package/dist/types/server/safeFetch.d.ts +135 -0
- package/dist/types/server/verifySecret.d.ts +29 -0
- package/dist/types/utils/fapiAutoDetect.d.ts +6 -23
- package/package.json +2 -1
- package/src/__tests__/authSocket.test.ts +96 -0
- package/src/i18n/locales/en-US.json +9 -0
- package/src/i18n/locales/es-ES.json +9 -0
- package/src/index.ts +1 -1
- package/src/mixins/OxyServices.assets.ts +40 -6
- package/src/mixins/OxyServices.utility.ts +57 -23
- package/src/mixins/__tests__/assetUpload.test.ts +191 -0
- package/src/mixins/__tests__/getFileDownloadUrl.test.ts +13 -0
- package/src/mixins/__tests__/serviceAuth.test.ts +30 -2
- package/src/server/__tests__/cors.test.ts +144 -0
- package/src/server/__tests__/safeFetch.test.ts +232 -0
- package/src/server/__tests__/verifySecret.test.ts +40 -0
- package/src/server/cors.ts +195 -0
- package/src/server/index.ts +30 -0
- package/src/server/safeFetch.ts +581 -0
- package/src/server/verifySecret.ts +52 -0
- package/src/utils/__tests__/fapiAutoDetect.test.ts +40 -11
- package/src/utils/fapiAutoDetect.ts +12 -39
|
@@ -129,6 +129,15 @@
|
|
|
129
129
|
"title": "Reputation = Trust & Growth",
|
|
130
130
|
"body": "Oxy Trust is a reputation system that reacts to what you do. Helpful, respectful, constructive actions earn it. Harmful or low‑effort stuff chips it away. More reputation can unlock benefits; low reputation can limit features. It keeps things fair and rewards real contribution."
|
|
131
131
|
},
|
|
132
|
+
"name": {
|
|
133
|
+
"title": "What's your name?",
|
|
134
|
+
"body": "Add your name so people know who you are.",
|
|
135
|
+
"firstLabel": "First name",
|
|
136
|
+
"firstPlaceholder": "Your first name",
|
|
137
|
+
"lastLabel": "Last name",
|
|
138
|
+
"lastPlaceholder": "Your last name",
|
|
139
|
+
"saveFailed": "Could not save your name"
|
|
140
|
+
},
|
|
132
141
|
"avatar": {
|
|
133
142
|
"title": "Make It Yours",
|
|
134
143
|
"body": "Add an avatar so people recognize you. It will show anywhere you show up here. Skip if you want — you can add it later.",
|
|
@@ -849,6 +849,15 @@
|
|
|
849
849
|
"title": "Reputación = Confianza y crecimiento",
|
|
850
850
|
"body": "Oxy Trust es un sistema de reputación que reacciona a lo que haces. Las acciones útiles, respetuosas y constructivas la aumentan. Las acciones dañinas o de poco esfuerzo la reducen. Más reputación puede desbloquear beneficios; poca reputación puede limitar funciones. Mantiene la justicia y recompensa la contribución real."
|
|
851
851
|
},
|
|
852
|
+
"name": {
|
|
853
|
+
"title": "¿Cuál es tu nombre?",
|
|
854
|
+
"body": "Añade tu nombre para que la gente sepa quién eres.",
|
|
855
|
+
"firstLabel": "Nombre",
|
|
856
|
+
"firstPlaceholder": "Tu nombre",
|
|
857
|
+
"lastLabel": "Apellidos",
|
|
858
|
+
"lastPlaceholder": "Tus apellidos",
|
|
859
|
+
"saveFailed": "No se pudo guardar tu nombre"
|
|
860
|
+
},
|
|
852
861
|
"avatar": {
|
|
853
862
|
"title": "Hazlo tuyo",
|
|
854
863
|
"body": "Añade un avatar para que te reconozcan. Se mostrará donde aparezcas aquí. Puedes omitirlo — puedes añadirlo más tarde.",
|
|
@@ -129,6 +129,15 @@
|
|
|
129
129
|
"title": "Reputation = Trust & Growth",
|
|
130
130
|
"body": "Oxy Trust is a reputation system that reacts to what you do. Helpful, respectful, constructive actions earn it. Harmful or low‑effort stuff chips it away. More reputation can unlock benefits; low reputation can limit features. It keeps things fair and rewards real contribution."
|
|
131
131
|
},
|
|
132
|
+
"name": {
|
|
133
|
+
"title": "What's your name?",
|
|
134
|
+
"body": "Add your name so people know who you are.",
|
|
135
|
+
"firstLabel": "First name",
|
|
136
|
+
"firstPlaceholder": "Your first name",
|
|
137
|
+
"lastLabel": "Last name",
|
|
138
|
+
"lastPlaceholder": "Your last name",
|
|
139
|
+
"saveFailed": "Could not save your name"
|
|
140
|
+
},
|
|
132
141
|
"avatar": {
|
|
133
142
|
"title": "Make It Yours",
|
|
134
143
|
"body": "Add an avatar so people recognize you. It will show anywhere you show up here. Skip if you want — you can add it later.",
|
|
@@ -849,6 +849,15 @@
|
|
|
849
849
|
"title": "Reputación = Confianza y crecimiento",
|
|
850
850
|
"body": "Oxy Trust es un sistema de reputación que reacciona a lo que haces. Las acciones útiles, respetuosas y constructivas la aumentan. Las acciones dañinas o de poco esfuerzo la reducen. Más reputación puede desbloquear beneficios; poca reputación puede limitar funciones. Mantiene la justicia y recompensa la contribución real."
|
|
851
851
|
},
|
|
852
|
+
"name": {
|
|
853
|
+
"title": "¿Cuál es tu nombre?",
|
|
854
|
+
"body": "Añade tu nombre para que la gente sepa quién eres.",
|
|
855
|
+
"firstLabel": "Nombre",
|
|
856
|
+
"firstPlaceholder": "Tu nombre",
|
|
857
|
+
"lastLabel": "Apellidos",
|
|
858
|
+
"lastPlaceholder": "Tus apellidos",
|
|
859
|
+
"saveFailed": "No se pudo guardar tu nombre"
|
|
860
|
+
},
|
|
852
861
|
"avatar": {
|
|
853
862
|
"title": "Hazlo tuyo",
|
|
854
863
|
"body": "Añade un avatar para que te reconozcan. Se mostrará donde aparezcas aquí. Puedes omitirlo — puedes añadirlo más tarde.",
|
package/dist/cjs/index.js
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.isRTLLocale = exports.normalizeLanguageCode = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.DeviceManager = exports.RecoveryPhraseService = exports.SignatureService = 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.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.OxyAppDataIdentifierError = exports.ServiceCredentialMismatchError = exports.createCrossDomainAuth = exports.CrossDomainAuth = exports.createAuthManager = exports.AuthManager = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyServices = void 0;
|
|
22
22
|
exports.isRequiredNumber = exports.isRequiredString = 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 = exports.createDebugLogger = exports.debugError = exports.debugWarn = exports.debugLog = exports.isDev = 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 = void 0;
|
|
23
|
-
exports.isCentralIdPOrigin = exports.buildSsoBounceUrl = exports.getSsoCallbackBootstrapScript = exports.ssoNavigate = exports.ssoCallbackBootstrapKey = exports.ssoAttemptedKey = exports.ssoNoSessionKey = exports.ssoDestKey = exports.ssoGuardKey = exports.ssoStateKey = exports.SSO_GUARD_TTL_MS = exports.SSO_CALLBACK_PATH = exports.generateSsoState = exports.consumeSsoReturn = exports.parseSsoReturnFragment = exports.resolveCentralAuthUrl = exports.CENTRAL_IDP_APEX = exports.CENTRAL_AUTH_URL = exports.
|
|
24
|
-
exports.packageInfo = exports.runColdBoot =
|
|
23
|
+
exports.guardActive = exports.isCentralIdPOrigin = exports.buildSsoBounceUrl = exports.getSsoCallbackBootstrapScript = exports.ssoNavigate = exports.ssoCallbackBootstrapKey = exports.ssoAttemptedKey = exports.ssoNoSessionKey = exports.ssoDestKey = exports.ssoGuardKey = exports.ssoStateKey = exports.SSO_GUARD_TTL_MS = exports.SSO_CALLBACK_PATH = exports.generateSsoState = exports.consumeSsoReturn = exports.parseSsoReturnFragment = exports.resolveCentralAuthUrl = exports.CENTRAL_IDP_APEX = exports.CENTRAL_AUTH_URL = exports.registrableApex = exports.autoDetectAuthWebUrl = exports.getAccountColor = exports.mergeAccountsFromRefreshAll = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.logPerformance = exports.logPayment = exports.logDevice = exports.logUser = exports.logSession = exports.logApi = exports.logAuth = exports.LogLevel = exports.logger = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = void 0;
|
|
24
|
+
exports.packageInfo = exports.runColdBoot = void 0;
|
|
25
25
|
// Ensure crypto polyfills are loaded before anything else
|
|
26
26
|
require("./crypto/polyfill");
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
@@ -242,7 +242,6 @@ Object.defineProperty(exports, "getAccountColor", { enumerable: true, get: funct
|
|
|
242
242
|
var fapiAutoDetect_1 = require("./utils/fapiAutoDetect");
|
|
243
243
|
Object.defineProperty(exports, "autoDetectAuthWebUrl", { enumerable: true, get: function () { return fapiAutoDetect_1.autoDetectAuthWebUrl; } });
|
|
244
244
|
Object.defineProperty(exports, "registrableApex", { enumerable: true, get: function () { return fapiAutoDetect_1.registrableApex; } });
|
|
245
|
-
Object.defineProperty(exports, "MULTIPART_TLDS", { enumerable: true, get: function () { return fapiAutoDetect_1.MULTIPART_TLDS; } });
|
|
246
245
|
// Central cross-domain SSO (opaque single-use code bounce via auth.oxy.so)
|
|
247
246
|
var authWebUrl_1 = require("./utils/authWebUrl");
|
|
248
247
|
Object.defineProperty(exports, "CENTRAL_AUTH_URL", { enumerable: true, get: function () { return authWebUrl_1.CENTRAL_AUTH_URL; } });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OxyServicesAssetsMixin = OxyServicesAssetsMixin;
|
|
4
|
+
const platform_1 = require("../utils/platform");
|
|
4
5
|
function OxyServicesAssetsMixin(Base) {
|
|
5
6
|
return class extends Base {
|
|
6
7
|
constructor(...args) {
|
|
@@ -43,8 +44,8 @@ function OxyServicesAssetsMixin(Base) {
|
|
|
43
44
|
*
|
|
44
45
|
* For a CDN-signed URL fetched from the API, use {@link getFileDownloadUrlAsync}.
|
|
45
46
|
*/
|
|
46
|
-
getFileDownloadUrl(fileId, variant, expiresIn) {
|
|
47
|
-
const token = this.getClient().getAccessToken();
|
|
47
|
+
getFileDownloadUrl(fileId, variant, expiresIn, options = {}) {
|
|
48
|
+
const token = options.omitToken ? undefined : this.getClient().getAccessToken();
|
|
48
49
|
// Public case: no auth token and no expiry requested → clean CDN URL.
|
|
49
50
|
// CloudFront serves the public media origin under `${cloudURL}/<id>`.
|
|
50
51
|
if (!token && !expiresIn) {
|
|
@@ -192,10 +193,32 @@ function OxyServicesAssetsMixin(Base) {
|
|
|
192
193
|
formData.append('file', file, fileName);
|
|
193
194
|
}
|
|
194
195
|
else if ('uri' in file && typeof file.uri === 'string') {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
const descriptor = file;
|
|
197
|
+
if ((0, platform_1.isReactNative)()) {
|
|
198
|
+
// React Native file descriptor — RN's FormData handles {uri, type, name} natively.
|
|
199
|
+
// It reads the file from disk during the multipart request — no in-JS Blob
|
|
200
|
+
// conversion (which would fail on Hermes for ArrayBuffer-backed Blobs).
|
|
201
|
+
formData.append('file', descriptor, fileName);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Web (browser/Node): the browser's FormData cannot read bytes from a plain
|
|
205
|
+
// { uri } object — it would serialize "[object Object]" and the server would
|
|
206
|
+
// store a 0-byte asset. Materialize the uri into a real Blob first. `fetch`
|
|
207
|
+
// resolves blob:, data:, and http(s): uris on web, so all picker outputs work.
|
|
208
|
+
const res = await fetch(descriptor.uri);
|
|
209
|
+
if (!res.ok) {
|
|
210
|
+
throw new Error(`Failed to read file from uri (status ${res.status})`);
|
|
211
|
+
}
|
|
212
|
+
const fetched = await res.blob();
|
|
213
|
+
// Preserve the descriptor's declared MIME type when the fetched blob has none.
|
|
214
|
+
const blob = fetched.type === '' && descriptor.type
|
|
215
|
+
? new Blob([fetched], { type: descriptor.type })
|
|
216
|
+
: fetched;
|
|
217
|
+
if (blob.size === 0) {
|
|
218
|
+
throw new Error('Cannot upload an empty file');
|
|
219
|
+
}
|
|
220
|
+
formData.append('file', blob, fileName);
|
|
221
|
+
}
|
|
199
222
|
}
|
|
200
223
|
else {
|
|
201
224
|
throw new Error('Unsupported file input: expected File, Blob, or { uri, type?, name?, size? } descriptor');
|
|
@@ -691,33 +691,45 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
691
691
|
}
|
|
692
692
|
return next(new Error('Invalid token'));
|
|
693
693
|
}
|
|
694
|
-
const
|
|
695
|
-
if (!
|
|
694
|
+
const claimedUserId = decoded.userId || decoded.id;
|
|
695
|
+
if (!claimedUserId) {
|
|
696
696
|
return next(new Error('Invalid token payload'));
|
|
697
697
|
}
|
|
698
698
|
// Check expiration — reject tokens at exact expiry second (use <=)
|
|
699
699
|
if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
|
|
700
700
|
return next(new Error('Token expired'));
|
|
701
701
|
}
|
|
702
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
702
|
+
// A server-validated session is mandatory. A bare decoded JWT proves
|
|
703
|
+
// nothing — the signature is not verified here, so without a session
|
|
704
|
+
// round-trip a forged token could claim any user id.
|
|
705
|
+
if (!decoded.sessionId) {
|
|
706
|
+
return next(new Error('Session required'));
|
|
707
|
+
}
|
|
708
|
+
let userId = claimedUserId;
|
|
709
|
+
try {
|
|
710
|
+
const result = await oxyInstance.validateSession(decoded.sessionId, {
|
|
711
|
+
useHeaderValidation: true,
|
|
712
|
+
});
|
|
713
|
+
if (!result || !result.valid || !result.user) {
|
|
714
|
+
return next(new Error('Session invalid'));
|
|
711
715
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
return next(new Error('Session validation failed'));
|
|
716
|
+
// The session is the source of truth. The client-claimed user id
|
|
717
|
+
// must match the server-validated identity, otherwise a valid
|
|
718
|
+
// session could be paired with a forged user id.
|
|
719
|
+
const validatedUserId = getUserIdentityId(result.user);
|
|
720
|
+
if (!validatedUserId || validatedUserId !== claimedUserId) {
|
|
721
|
+
return next(new Error('Session user mismatch'));
|
|
720
722
|
}
|
|
723
|
+
userId = validatedUserId;
|
|
724
|
+
}
|
|
725
|
+
catch (validateErr) {
|
|
726
|
+
if (debug) {
|
|
727
|
+
loggerUtils_1.logger.debug('[oxy.authSocket] Session validation failed', {
|
|
728
|
+
component: 'auth',
|
|
729
|
+
method: 'authSocket',
|
|
730
|
+
}, validateErr);
|
|
731
|
+
}
|
|
732
|
+
return next(new Error('Session validation failed'));
|
|
721
733
|
}
|
|
722
734
|
// Attach user data to socket. We expose BOTH `socket.data.userId`
|
|
723
735
|
// (the official Socket.IO data slot) and `socket.user` because
|
|
@@ -782,9 +794,9 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
782
794
|
* Express.js middleware that enforces a specific service-token scope.
|
|
783
795
|
*
|
|
784
796
|
* Mount AFTER `auth()` / `serviceAuth()` — relies on `req.serviceApp` and
|
|
785
|
-
* (when delegation is in effect) `req.serviceActingAs.scopes`.
|
|
786
|
-
*
|
|
787
|
-
* the app
|
|
797
|
+
* (when delegation is in effect) `req.serviceActingAs.scopes`. App-only
|
|
798
|
+
* service requests require the app scope. Delegated user requests require
|
|
799
|
+
* BOTH the app scope and the per-user delegation scope.
|
|
788
800
|
*
|
|
789
801
|
* Requests authenticated as a regular user (no service token) are rejected
|
|
790
802
|
* with 403 — scope-protected endpoints are service-to-service by design.
|
|
@@ -814,7 +826,12 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
814
826
|
});
|
|
815
827
|
return;
|
|
816
828
|
}
|
|
817
|
-
|
|
829
|
+
const appHasScope = appScopes.includes(scope);
|
|
830
|
+
const delegationHasScope = delegatedScopes.includes(scope);
|
|
831
|
+
const hasRequiredScope = req.serviceActingAs
|
|
832
|
+
? appHasScope && delegationHasScope
|
|
833
|
+
: appHasScope;
|
|
834
|
+
if (hasRequiredScope) {
|
|
818
835
|
next();
|
|
819
836
|
return;
|
|
820
837
|
}
|
|
@@ -872,6 +889,18 @@ async function verifyServiceTokenSignature(token, secret) {
|
|
|
872
889
|
* access token signed by the same shared secret could be replayed as a
|
|
873
890
|
* service token because no claim binding existed.
|
|
874
891
|
*/
|
|
892
|
+
/**
|
|
893
|
+
* Resolve the canonical user id from a validated session's user object.
|
|
894
|
+
*
|
|
895
|
+
* The API serializer emits `id`, but some upstream shapes carry the raw Mongo
|
|
896
|
+
* `_id` instead. We accept either, but only a non-empty string — anything else
|
|
897
|
+
* means the validated identity is unusable and the caller must reject.
|
|
898
|
+
*/
|
|
899
|
+
function getUserIdentityId(user) {
|
|
900
|
+
const candidate = user.id
|
|
901
|
+
?? user._id;
|
|
902
|
+
return typeof candidate === 'string' && candidate.length > 0 ? candidate : null;
|
|
903
|
+
}
|
|
875
904
|
function verifyServiceTokenClaims(decoded, expected) {
|
|
876
905
|
if (decoded.type !== 'service') {
|
|
877
906
|
throw new ServiceTokenClaimError(`Service token has unexpected type '${String(decoded.type)}'`);
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Strict CORS allowlist for Oxy backends.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS
|
|
6
|
+
* ---------------
|
|
7
|
+
* App backends kept hand-rolling CORS, and the unsafe patterns recurred:
|
|
8
|
+
* - `Access-Control-Allow-Origin: *` together with credentials (which is
|
|
9
|
+
* spec-invalid AND a credential-leak vector), or
|
|
10
|
+
* - a "reflect whatever Origin the request carried" fallback (effectively
|
|
11
|
+
* `*` for credentialed requests — the Allo wildcard-fallback class).
|
|
12
|
+
*
|
|
13
|
+
* `createOxyCors` returns a self-contained Express middleware (no `cors`
|
|
14
|
+
* package dependency) that:
|
|
15
|
+
* - allows the Oxy apex origin family (anything under `*.${CENTRAL_IDP_APEX}`,
|
|
16
|
+
* i.e. `oxy.so` — covering `auth.oxy.so`, `api.oxy.so`, `accounts.oxy.so`,
|
|
17
|
+
* `console.oxy.so`, `inbox.oxy.so`, the marketing site, …) reusing the
|
|
18
|
+
* central-origin constants already in core, NOT a fresh hardcoded list,
|
|
19
|
+
* - allows the caller's explicit `appOrigins`,
|
|
20
|
+
* - DENIES everything else (no reflection, never a wildcard with credentials),
|
|
21
|
+
* - echoes back the EXACT matched origin (so credentialed requests work) and
|
|
22
|
+
* sets `Vary: Origin` for correct caching,
|
|
23
|
+
* - answers CORS preflight (`OPTIONS`) with `204`.
|
|
24
|
+
*
|
|
25
|
+
* Node/Express-only: exported solely from `@oxyhq/core/server`.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.createOxyCors = createOxyCors;
|
|
29
|
+
const authWebUrl_1 = require("../utils/authWebUrl");
|
|
30
|
+
const fapiAutoDetect_1 = require("../utils/fapiAutoDetect");
|
|
31
|
+
/** Default HTTP methods allowed across origins. */
|
|
32
|
+
const DEFAULT_ALLOWED_METHODS = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'];
|
|
33
|
+
/** Default request headers a browser may send on a credentialed cross-origin call. */
|
|
34
|
+
const DEFAULT_ALLOWED_HEADERS = [
|
|
35
|
+
'Content-Type',
|
|
36
|
+
'Authorization',
|
|
37
|
+
'X-Requested-With',
|
|
38
|
+
'X-Oxy-User-Id',
|
|
39
|
+
'X-Oxy-Internal',
|
|
40
|
+
'X-CSRF-Token',
|
|
41
|
+
];
|
|
42
|
+
/** How long (seconds) a browser may cache a successful preflight. */
|
|
43
|
+
const DEFAULT_MAX_AGE_SECONDS = 86400;
|
|
44
|
+
/**
|
|
45
|
+
* Whether `candidate` belongs to the Oxy apex origin family — i.e. its
|
|
46
|
+
* registrable apex equals {@link CENTRAL_IDP_APEX} (`oxy.so`). This matches the
|
|
47
|
+
* apex itself (`https://oxy.so`) and any subdomain (`https://auth.oxy.so`,
|
|
48
|
+
* `https://api.oxy.so`, …) over http or https, ports allowed. Returns false on
|
|
49
|
+
* any parse failure (fail closed).
|
|
50
|
+
*/
|
|
51
|
+
function isOxyFamilyOrigin(candidate) {
|
|
52
|
+
let hostname;
|
|
53
|
+
let protocol;
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(candidate);
|
|
56
|
+
hostname = url.hostname.toLowerCase();
|
|
57
|
+
protocol = url.protocol;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (protocol !== 'https:' && protocol !== 'http:')
|
|
63
|
+
return false;
|
|
64
|
+
if (hostname === authWebUrl_1.CENTRAL_IDP_APEX)
|
|
65
|
+
return true;
|
|
66
|
+
return (0, fapiAutoDetect_1.registrableApex)(hostname) === authWebUrl_1.CENTRAL_IDP_APEX;
|
|
67
|
+
}
|
|
68
|
+
/** Normalize a raw origin string to its canonical `scheme://host[:port]` form. */
|
|
69
|
+
function normalizeOrigin(raw) {
|
|
70
|
+
try {
|
|
71
|
+
return new URL(raw).origin;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build the origin-matching predicate: true iff `origin` is in the Oxy apex
|
|
79
|
+
* family OR exactly matches one of the configured app origins.
|
|
80
|
+
*/
|
|
81
|
+
function buildOriginAllowed(appOrigins) {
|
|
82
|
+
const explicit = new Set();
|
|
83
|
+
for (const raw of appOrigins) {
|
|
84
|
+
const normalized = normalizeOrigin(raw);
|
|
85
|
+
if (normalized)
|
|
86
|
+
explicit.add(normalized);
|
|
87
|
+
}
|
|
88
|
+
return (origin) => {
|
|
89
|
+
const normalized = normalizeOrigin(origin);
|
|
90
|
+
if (normalized === null)
|
|
91
|
+
return false;
|
|
92
|
+
if (explicit.has(normalized))
|
|
93
|
+
return true;
|
|
94
|
+
return isOxyFamilyOrigin(normalized);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Create a strict Oxy CORS middleware. See module docs.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* app.use(createOxyCors({ appOrigins: ['https://app.example.com'] }));
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
function createOxyCors(options = {}) {
|
|
106
|
+
const { appOrigins = [], allowCredentials = true, methods = DEFAULT_ALLOWED_METHODS, allowedHeaders = DEFAULT_ALLOWED_HEADERS, exposedHeaders = [], maxAgeSeconds = DEFAULT_MAX_AGE_SECONDS, } = options;
|
|
107
|
+
const isOriginAllowed = buildOriginAllowed(appOrigins);
|
|
108
|
+
const methodsHeader = methods.join(', ');
|
|
109
|
+
const allowedHeadersHeader = allowedHeaders.join(', ');
|
|
110
|
+
const exposedHeadersHeader = exposedHeaders.join(', ');
|
|
111
|
+
return (req, res, next) => {
|
|
112
|
+
const origin = req.headers.origin;
|
|
113
|
+
// Same-origin or non-browser requests carry no Origin header — pass through
|
|
114
|
+
// untouched (no ACAO header is emitted, which is correct for them).
|
|
115
|
+
if (typeof origin !== 'string' || origin.length === 0) {
|
|
116
|
+
if (req.method === 'OPTIONS') {
|
|
117
|
+
res.sendStatus(204);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
next();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Origin is present. Caching correctness: this response varies by Origin.
|
|
124
|
+
res.setHeader('Vary', 'Origin');
|
|
125
|
+
if (!isOriginAllowed(origin)) {
|
|
126
|
+
// DENY: do NOT reflect the origin, do NOT emit a wildcard. The browser
|
|
127
|
+
// will block the cross-origin read. Preflights for denied origins get a
|
|
128
|
+
// 204 with no CORS headers (the actual request then fails CORS).
|
|
129
|
+
if (req.method === 'OPTIONS') {
|
|
130
|
+
res.sendStatus(204);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
next();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// ALLOW: echo the EXACT matched origin — never `*`, even without credentials.
|
|
137
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
138
|
+
if (allowCredentials) {
|
|
139
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
140
|
+
}
|
|
141
|
+
if (exposedHeadersHeader) {
|
|
142
|
+
res.setHeader('Access-Control-Expose-Headers', exposedHeadersHeader);
|
|
143
|
+
}
|
|
144
|
+
if (req.method === 'OPTIONS') {
|
|
145
|
+
res.setHeader('Access-Control-Allow-Methods', methodsHeader);
|
|
146
|
+
// Honour the browser's requested headers when present, else the default set.
|
|
147
|
+
const requested = req.headers['access-control-request-headers'];
|
|
148
|
+
res.setHeader('Access-Control-Allow-Headers', typeof requested === 'string' && requested.length > 0 ? requested : allowedHeadersHeader);
|
|
149
|
+
res.setHeader('Access-Control-Max-Age', String(maxAgeSeconds));
|
|
150
|
+
res.sendStatus(204);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
next();
|
|
154
|
+
};
|
|
155
|
+
}
|
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.createOxyRateLimit = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getOxyUserId = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = void 0;
|
|
19
|
+
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.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; } });
|
|
@@ -26,3 +26,23 @@ Object.defineProperty(exports, "isOxyAuthenticated", { enumerable: true, get: fu
|
|
|
26
26
|
Object.defineProperty(exports, "requireOxyAuth", { enumerable: true, get: function () { return auth_1.requireOxyAuth; } });
|
|
27
27
|
var rateLimit_1 = require("./rateLimit");
|
|
28
28
|
Object.defineProperty(exports, "createOxyRateLimit", { enumerable: true, get: function () { return rateLimit_1.createOxyRateLimit; } });
|
|
29
|
+
// SSRF-safe upstream fetch + URL validation (Node-only).
|
|
30
|
+
var safeFetch_1 = require("./safeFetch");
|
|
31
|
+
Object.defineProperty(exports, "assertSafePublicUrl", { enumerable: true, get: function () { return safeFetch_1.assertSafePublicUrl; } });
|
|
32
|
+
Object.defineProperty(exports, "isBlockedIp", { enumerable: true, get: function () { return safeFetch_1.isBlockedIp; } });
|
|
33
|
+
Object.defineProperty(exports, "safeFetch", { enumerable: true, get: function () { return safeFetch_1.safeFetch; } });
|
|
34
|
+
Object.defineProperty(exports, "SsrfRejection", { enumerable: true, get: function () { return safeFetch_1.SsrfRejection; } });
|
|
35
|
+
Object.defineProperty(exports, "UpstreamError", { enumerable: true, get: function () { return safeFetch_1.UpstreamError; } });
|
|
36
|
+
Object.defineProperty(exports, "ALLOWED_PORTS", { enumerable: true, get: function () { return safeFetch_1.ALLOWED_PORTS; } });
|
|
37
|
+
Object.defineProperty(exports, "ALLOWED_PROTOCOLS", { enumerable: true, get: function () { return safeFetch_1.ALLOWED_PROTOCOLS; } });
|
|
38
|
+
Object.defineProperty(exports, "BLOCKED_HOSTNAMES", { enumerable: true, get: function () { return safeFetch_1.BLOCKED_HOSTNAMES; } });
|
|
39
|
+
Object.defineProperty(exports, "DEFAULT_USER_AGENT", { enumerable: true, get: function () { return safeFetch_1.DEFAULT_USER_AGENT; } });
|
|
40
|
+
Object.defineProperty(exports, "MAX_REDIRECTS", { enumerable: true, get: function () { return safeFetch_1.MAX_REDIRECTS; } });
|
|
41
|
+
Object.defineProperty(exports, "MAX_URL_LENGTH", { enumerable: true, get: function () { return safeFetch_1.MAX_URL_LENGTH; } });
|
|
42
|
+
Object.defineProperty(exports, "UPSTREAM_HEADERS_TIMEOUT_MS", { enumerable: true, get: function () { return safeFetch_1.UPSTREAM_HEADERS_TIMEOUT_MS; } });
|
|
43
|
+
// Strict CORS allowlist (Oxy apex family + explicit app origins).
|
|
44
|
+
var cors_1 = require("./cors");
|
|
45
|
+
Object.defineProperty(exports, "createOxyCors", { enumerable: true, get: function () { return cors_1.createOxyCors; } });
|
|
46
|
+
// Constant-time secret comparison.
|
|
47
|
+
var verifySecret_1 = require("./verifySecret");
|
|
48
|
+
Object.defineProperty(exports, "verifySecret", { enumerable: true, get: function () { return verifySecret_1.verifySecret; } });
|