@oxyhq/core 16.0.0 → 17.0.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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +28 -6
- package/dist/cjs/i18n/locales/en-US.json +5 -3
- package/dist/cjs/i18n/locales/es-ES.json +5 -3
- package/dist/cjs/i18n/locales/locales/en-US.json +5 -3
- package/dist/cjs/i18n/locales/locales/es-ES.json +5 -3
- package/dist/cjs/index.js +6 -3
- package/dist/cjs/mixins/OxyServices.auth.js +57 -19
- package/dist/cjs/server/index.js +7 -1
- package/dist/cjs/server/userInvalidation.js +172 -0
- package/dist/cjs/utils/displayNamePolicyRanges.generated.js +28 -3
- package/dist/cjs/utils/validationUtils.js +112 -21
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +28 -6
- package/dist/esm/i18n/locales/en-US.json +5 -3
- package/dist/esm/i18n/locales/es-ES.json +5 -3
- package/dist/esm/i18n/locales/locales/en-US.json +5 -3
- package/dist/esm/i18n/locales/locales/es-ES.json +5 -3
- package/dist/esm/index.js +1 -1
- package/dist/esm/mixins/OxyServices.auth.js +57 -19
- package/dist/esm/server/index.js +3 -0
- package/dist/esm/server/userInvalidation.js +167 -0
- package/dist/esm/utils/displayNamePolicyRanges.generated.js +27 -2
- package/dist/esm/utils/validationUtils.js +112 -21
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +13 -4
- package/dist/types/index.d.ts +2 -2
- package/dist/types/mixins/OxyServices.auth.d.ts +19 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/userInvalidation.d.ts +133 -0
- package/dist/types/utils/displayNamePolicyRanges.generated.d.ts +27 -2
- package/dist/types/utils/validationUtils.d.ts +106 -20
- package/package.json +1 -1
- package/src/HttpService.ts +32 -7
- package/src/__tests__/httpServiceFormEncoded.test.ts +142 -0
- package/src/i18n/locales/en-US.json +5 -3
- package/src/i18n/locales/es-ES.json +5 -3
- package/src/index.ts +4 -1
- package/src/mixins/OxyServices.auth.ts +76 -20
- package/src/mixins/__tests__/preSessionSkipAuth.test.ts +72 -11
- package/src/server/__tests__/userInvalidation.test.ts +208 -0
- package/src/server/index.ts +13 -0
- package/src/server/userInvalidation.ts +221 -0
- package/src/utils/__tests__/coldBoot.test.ts +9 -4
- package/src/utils/__tests__/validationUtils.test.ts +292 -1
- package/src/utils/displayNamePolicyRanges.generated.ts +31 -2
- package/src/utils/validationUtils.ts +117 -20
package/dist/cjs/HttpService.js
CHANGED
|
@@ -238,10 +238,9 @@ class HttpService {
|
|
|
238
238
|
*
|
|
239
239
|
* Why we explicitly reject `URLSearchParams`:
|
|
240
240
|
* - `URLSearchParams` ALSO exposes `append` / `get` / `has`, so the
|
|
241
|
-
* duck-type fallback below would have misidentified it as FormData
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* of an empty multipart body.
|
|
241
|
+
* duck-type fallback below would have misidentified it as FormData and
|
|
242
|
+
* sent an empty multipart body.
|
|
243
|
+
* - It has its own encoding path instead — see {@link isUrlSearchParams}.
|
|
245
244
|
*/
|
|
246
245
|
isFormData(data) {
|
|
247
246
|
if (!data || typeof data !== 'object') {
|
|
@@ -275,6 +274,18 @@ class HttpService {
|
|
|
275
274
|
typeof candidate.getAll === 'function' &&
|
|
276
275
|
typeof candidate.delete === 'function');
|
|
277
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* True for an `application/x-www-form-urlencoded` payload.
|
|
279
|
+
*
|
|
280
|
+
* Needed because a handful of endpoints are defined by a standard that fixes
|
|
281
|
+
* their request encoding rather than by our own JSON conventions — today
|
|
282
|
+
* `POST /auth/oauth/token`, whose encoding RFC 6749 §4.1.3 mandates. Passing
|
|
283
|
+
* a `URLSearchParams` as `data` selects that encoding; everything else is
|
|
284
|
+
* still JSON.
|
|
285
|
+
*/
|
|
286
|
+
isUrlSearchParams(data) {
|
|
287
|
+
return typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams;
|
|
288
|
+
}
|
|
278
289
|
/**
|
|
279
290
|
* Main request method - handles everything in one place
|
|
280
291
|
*/
|
|
@@ -322,6 +333,7 @@ class HttpService {
|
|
|
322
333
|
const fullUrl = this.buildURL(url, params);
|
|
323
334
|
// Determine if data is FormData using robust detection
|
|
324
335
|
const isFormData = this.isFormData(data);
|
|
336
|
+
const isUrlEncoded = this.isUrlSearchParams(data);
|
|
325
337
|
// Make fetch request
|
|
326
338
|
const controller = new AbortController();
|
|
327
339
|
const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : null;
|
|
@@ -333,7 +345,10 @@ class HttpService {
|
|
|
333
345
|
'Accept': 'application/json',
|
|
334
346
|
};
|
|
335
347
|
// Only set Content-Type for non-FormData requests (FormData sets it automatically with boundary)
|
|
336
|
-
if (
|
|
348
|
+
if (isUrlEncoded) {
|
|
349
|
+
headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
|
|
350
|
+
}
|
|
351
|
+
else if (!isFormData) {
|
|
337
352
|
headers['Content-Type'] = 'application/json';
|
|
338
353
|
}
|
|
339
354
|
// Add authorization header if available
|
|
@@ -375,8 +390,11 @@ class HttpService {
|
|
|
375
390
|
headers[key] = value;
|
|
376
391
|
});
|
|
377
392
|
}
|
|
393
|
+
// `URLSearchParams` is serialised explicitly rather than handed to
|
|
394
|
+
// `fetch` as-is: RN's fetch does not consistently encode it, and doing
|
|
395
|
+
// it here keeps the body identical across every platform.
|
|
378
396
|
const bodyValue = method !== 'GET' && data
|
|
379
|
-
? (isFormData ? data : JSON.stringify(data))
|
|
397
|
+
? (isFormData ? data : isUrlEncoded ? data.toString() : JSON.stringify(data))
|
|
380
398
|
: undefined;
|
|
381
399
|
// React Native FormData workaround:
|
|
382
400
|
// Expo SDK 56's "winter fetch" rejects RN file descriptors `{uri, type, name}`
|
|
@@ -442,6 +460,10 @@ class HttpService {
|
|
|
442
460
|
if (errorData?.message) {
|
|
443
461
|
errorMessage = errorData.message;
|
|
444
462
|
}
|
|
463
|
+
else if (errorData?.error_description) {
|
|
464
|
+
// RFC 6749 §5.2 / RFC 6750 §3 — OAuth endpoints surface human text here.
|
|
465
|
+
errorMessage = errorData.error_description;
|
|
466
|
+
}
|
|
445
467
|
else if (errorData?.error) {
|
|
446
468
|
errorMessage = errorData.error;
|
|
447
469
|
}
|
|
@@ -678,7 +678,7 @@
|
|
|
678
678
|
"firstNamePlaceholder": "Enter first name",
|
|
679
679
|
"lastName": "Last Name",
|
|
680
680
|
"lastNamePlaceholder": "Enter last name (optional)",
|
|
681
|
-
"invalidChars": "Use letters and
|
|
681
|
+
"invalidChars": "Use letters, spaces, apostrophes, and name separators only"
|
|
682
682
|
},
|
|
683
683
|
"username": {
|
|
684
684
|
"title": "Username",
|
|
@@ -1752,7 +1752,8 @@
|
|
|
1752
1752
|
"title": "Create account",
|
|
1753
1753
|
"subtitle": "Create an account you control. It will have its own profile, members, and apps.",
|
|
1754
1754
|
"displayName": {
|
|
1755
|
-
"label": "Display name"
|
|
1755
|
+
"label": "Display name",
|
|
1756
|
+
"invalidChars": "Use letters, spaces, apostrophes, and name separators only"
|
|
1756
1757
|
},
|
|
1757
1758
|
"bio": {
|
|
1758
1759
|
"label": "Bio (optional)"
|
|
@@ -1829,7 +1830,8 @@
|
|
|
1829
1830
|
"subtitle": "Manage this account’s profile, members, and access.",
|
|
1830
1831
|
"save": "Save changes",
|
|
1831
1832
|
"displayName": {
|
|
1832
|
-
"label": "Display name"
|
|
1833
|
+
"label": "Display name",
|
|
1834
|
+
"invalidChars": "Use letters, spaces, apostrophes, and name separators only"
|
|
1833
1835
|
},
|
|
1834
1836
|
"bio": {
|
|
1835
1837
|
"label": "Bio (optional)"
|
|
@@ -203,7 +203,7 @@
|
|
|
203
203
|
"firstNamePlaceholder": "Introduce tu nombre",
|
|
204
204
|
"lastName": "Apellido",
|
|
205
205
|
"lastNamePlaceholder": "Introduce tu apellido (opcional)",
|
|
206
|
-
"invalidChars": "Usa solo letras y
|
|
206
|
+
"invalidChars": "Usa solo letras, espacios, apóstrofos y separadores de nombre"
|
|
207
207
|
},
|
|
208
208
|
"username": {
|
|
209
209
|
"title": "Usuario",
|
|
@@ -1752,7 +1752,8 @@
|
|
|
1752
1752
|
"title": "Crear cuenta",
|
|
1753
1753
|
"subtitle": "Crea una cuenta que tú controlas. Tendrá su propio perfil, miembros y aplicaciones.",
|
|
1754
1754
|
"displayName": {
|
|
1755
|
-
"label": "Nombre visible"
|
|
1755
|
+
"label": "Nombre visible",
|
|
1756
|
+
"invalidChars": "Usa solo letras, espacios, apóstrofos y separadores de nombre"
|
|
1756
1757
|
},
|
|
1757
1758
|
"bio": {
|
|
1758
1759
|
"label": "Biografía (opcional)"
|
|
@@ -1829,7 +1830,8 @@
|
|
|
1829
1830
|
"subtitle": "Gestiona el perfil, los miembros y el acceso de esta cuenta.",
|
|
1830
1831
|
"save": "Guardar cambios",
|
|
1831
1832
|
"displayName": {
|
|
1832
|
-
"label": "Nombre visible"
|
|
1833
|
+
"label": "Nombre visible",
|
|
1834
|
+
"invalidChars": "Usa solo letras, espacios, apóstrofos y separadores de nombre"
|
|
1833
1835
|
},
|
|
1834
1836
|
"bio": {
|
|
1835
1837
|
"label": "Biografía (opcional)"
|
|
@@ -678,7 +678,7 @@
|
|
|
678
678
|
"firstNamePlaceholder": "Enter first name",
|
|
679
679
|
"lastName": "Last Name",
|
|
680
680
|
"lastNamePlaceholder": "Enter last name (optional)",
|
|
681
|
-
"invalidChars": "Use letters and
|
|
681
|
+
"invalidChars": "Use letters, spaces, apostrophes, and name separators only"
|
|
682
682
|
},
|
|
683
683
|
"username": {
|
|
684
684
|
"title": "Username",
|
|
@@ -1752,7 +1752,8 @@
|
|
|
1752
1752
|
"title": "Create account",
|
|
1753
1753
|
"subtitle": "Create an account you control. It will have its own profile, members, and apps.",
|
|
1754
1754
|
"displayName": {
|
|
1755
|
-
"label": "Display name"
|
|
1755
|
+
"label": "Display name",
|
|
1756
|
+
"invalidChars": "Use letters, spaces, apostrophes, and name separators only"
|
|
1756
1757
|
},
|
|
1757
1758
|
"bio": {
|
|
1758
1759
|
"label": "Bio (optional)"
|
|
@@ -1829,7 +1830,8 @@
|
|
|
1829
1830
|
"subtitle": "Manage this account’s profile, members, and access.",
|
|
1830
1831
|
"save": "Save changes",
|
|
1831
1832
|
"displayName": {
|
|
1832
|
-
"label": "Display name"
|
|
1833
|
+
"label": "Display name",
|
|
1834
|
+
"invalidChars": "Use letters, spaces, apostrophes, and name separators only"
|
|
1833
1835
|
},
|
|
1834
1836
|
"bio": {
|
|
1835
1837
|
"label": "Bio (optional)"
|
|
@@ -203,7 +203,7 @@
|
|
|
203
203
|
"firstNamePlaceholder": "Introduce tu nombre",
|
|
204
204
|
"lastName": "Apellido",
|
|
205
205
|
"lastNamePlaceholder": "Introduce tu apellido (opcional)",
|
|
206
|
-
"invalidChars": "Usa solo letras y
|
|
206
|
+
"invalidChars": "Usa solo letras, espacios, apóstrofos y separadores de nombre"
|
|
207
207
|
},
|
|
208
208
|
"username": {
|
|
209
209
|
"title": "Usuario",
|
|
@@ -1752,7 +1752,8 @@
|
|
|
1752
1752
|
"title": "Crear cuenta",
|
|
1753
1753
|
"subtitle": "Crea una cuenta que tú controlas. Tendrá su propio perfil, miembros y aplicaciones.",
|
|
1754
1754
|
"displayName": {
|
|
1755
|
-
"label": "Nombre visible"
|
|
1755
|
+
"label": "Nombre visible",
|
|
1756
|
+
"invalidChars": "Usa solo letras, espacios, apóstrofos y separadores de nombre"
|
|
1756
1757
|
},
|
|
1757
1758
|
"bio": {
|
|
1758
1759
|
"label": "Biografía (opcional)"
|
|
@@ -1829,7 +1830,8 @@
|
|
|
1829
1830
|
"subtitle": "Gestiona el perfil, los miembros y el acceso de esta cuenta.",
|
|
1830
1831
|
"save": "Guardar cambios",
|
|
1831
1832
|
"displayName": {
|
|
1832
|
-
"label": "Nombre visible"
|
|
1833
|
+
"label": "Nombre visible",
|
|
1834
|
+
"invalidChars": "Usa solo letras, espacios, apóstrofos y separadores de nombre"
|
|
1833
1835
|
},
|
|
1834
1836
|
"bio": {
|
|
1835
1837
|
"label": "Biografía (opcional)"
|
package/dist/cjs/index.js
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
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.ServiceAssetMetadataError = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.AssetUrlResolutionError = exports.OxyServices = void 0;
|
|
22
22
|
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 = exports.TopicType = void 0;
|
|
23
|
-
exports.
|
|
24
|
-
exports.
|
|
25
|
-
exports.packageInfo = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshDeviceSecretArm = void 0;
|
|
23
|
+
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_UNFLANKED_SEPARATOR_SOURCE = 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.DISPLAY_NAME_INVALID_MESSAGE = exports.MAX_DISPLAY_NAME_LENGTH = 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 = exports.withRetry = void 0;
|
|
24
|
+
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 = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = 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 = void 0;
|
|
26
26
|
// Ensure crypto polyfills are loaded before anything else
|
|
27
27
|
require("./crypto/polyfill");
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
@@ -242,6 +242,8 @@ var validationUtils_1 = require("./utils/validationUtils");
|
|
|
242
242
|
Object.defineProperty(exports, "EMAIL_REGEX", { enumerable: true, get: function () { return validationUtils_1.EMAIL_REGEX; } });
|
|
243
243
|
Object.defineProperty(exports, "USERNAME_REGEX", { enumerable: true, get: function () { return validationUtils_1.USERNAME_REGEX; } });
|
|
244
244
|
Object.defineProperty(exports, "PASSWORD_REGEX", { enumerable: true, get: function () { return validationUtils_1.PASSWORD_REGEX; } });
|
|
245
|
+
Object.defineProperty(exports, "MAX_DISPLAY_NAME_LENGTH", { enumerable: true, get: function () { return validationUtils_1.MAX_DISPLAY_NAME_LENGTH; } });
|
|
246
|
+
Object.defineProperty(exports, "DISPLAY_NAME_INVALID_MESSAGE", { enumerable: true, get: function () { return validationUtils_1.DISPLAY_NAME_INVALID_MESSAGE; } });
|
|
245
247
|
Object.defineProperty(exports, "isValidEmail", { enumerable: true, get: function () { return validationUtils_1.isValidEmail; } });
|
|
246
248
|
Object.defineProperty(exports, "isValidUsername", { enumerable: true, get: function () { return validationUtils_1.isValidUsername; } });
|
|
247
249
|
Object.defineProperty(exports, "isValidPassword", { enumerable: true, get: function () { return validationUtils_1.isValidPassword; } });
|
|
@@ -249,6 +251,7 @@ Object.defineProperty(exports, "isValidDisplayName", { enumerable: true, get: fu
|
|
|
249
251
|
Object.defineProperty(exports, "DISPLAY_NAME_ALLOWED_SCRIPTS", { enumerable: true, get: function () { return validationUtils_1.DISPLAY_NAME_ALLOWED_SCRIPTS; } });
|
|
250
252
|
Object.defineProperty(exports, "DISPLAY_NAME_DISALLOWED_SOURCE", { enumerable: true, get: function () { return validationUtils_1.DISPLAY_NAME_DISALLOWED_SOURCE; } });
|
|
251
253
|
Object.defineProperty(exports, "DISPLAY_NAME_ORPHANED_MARK_SOURCE", { enumerable: true, get: function () { return validationUtils_1.DISPLAY_NAME_ORPHANED_MARK_SOURCE; } });
|
|
254
|
+
Object.defineProperty(exports, "DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE", { enumerable: true, get: function () { return validationUtils_1.DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE; } });
|
|
252
255
|
Object.defineProperty(exports, "isRequiredString", { enumerable: true, get: function () { return validationUtils_1.isRequiredString; } });
|
|
253
256
|
Object.defineProperty(exports, "isRequiredNumber", { enumerable: true, get: function () { return validationUtils_1.isRequiredNumber; } });
|
|
254
257
|
Object.defineProperty(exports, "isRequiredBoolean", { enumerable: true, get: function () { return validationUtils_1.isRequiredBoolean; } });
|
|
@@ -18,6 +18,12 @@ const userIdentity_1 = require("../utils/userIdentity");
|
|
|
18
18
|
* returned `expiresAt` is authoritative; this is only the client-proposed value.
|
|
19
19
|
*/
|
|
20
20
|
const COMMONS_SIGN_IN_EXPIRY_MS = 5 * 60 * 1000;
|
|
21
|
+
/**
|
|
22
|
+
* Fallback access-token lifetime used only if the token endpoint ever omits the
|
|
23
|
+
* RFC 6749 `expires_in` member. Matches the server's current 15-minute access
|
|
24
|
+
* token; the server's value always wins when present.
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_ACCESS_TOKEN_TTL_SECONDS = 15 * 60;
|
|
21
27
|
/**
|
|
22
28
|
* @internal Narrow an untrusted `requesterLabel` from the approve-info response.
|
|
23
29
|
*
|
|
@@ -1134,39 +1140,43 @@ function OxyServicesAuthMixin(Base) {
|
|
|
1134
1140
|
* after sign-in at auth.oxy.so) for a device-first session.
|
|
1135
1141
|
* Public first-party clients use PKCE (`codeVerifier`); the access token is
|
|
1136
1142
|
* planted immediately on success.
|
|
1143
|
+
*
|
|
1144
|
+
* Speaks the standard RFC 6749 §4.1.3 token request — a form-urlencoded
|
|
1145
|
+
* body with snake_case parameters and `grant_type=authorization_code` — and
|
|
1146
|
+
* reads the flat §5.1 response. The camelCase JSON request and `{ data }`
|
|
1147
|
+
* response this method used before were an Oxy invention no OAuth library
|
|
1148
|
+
* could interoperate with; the endpoint no longer accepts them. The method's
|
|
1149
|
+
* OWN signature is unchanged, so callers are unaffected.
|
|
1137
1150
|
*/
|
|
1138
1151
|
async exchangeOAuthCode(params) {
|
|
1139
1152
|
try {
|
|
1140
|
-
const
|
|
1153
|
+
const form = new URLSearchParams({
|
|
1154
|
+
grant_type: 'authorization_code',
|
|
1141
1155
|
code: params.code,
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
}
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
if (!payload || typeof payload !== 'object') {
|
|
1156
|
+
redirect_uri: params.redirectUri,
|
|
1157
|
+
client_id: params.clientId,
|
|
1158
|
+
code_verifier: params.codeVerifier,
|
|
1159
|
+
});
|
|
1160
|
+
const res = await this.makeRequest('POST', '/auth/oauth/token', form, { cache: false, skipAuth: true });
|
|
1161
|
+
if (!res || typeof res !== 'object') {
|
|
1149
1162
|
throw new Error('auth/oauth/token returned an unexpected response shape');
|
|
1150
1163
|
}
|
|
1151
|
-
|
|
1152
|
-
const
|
|
1153
|
-
const
|
|
1154
|
-
const
|
|
1155
|
-
const
|
|
1164
|
+
// RFC 6749 §5.1: every member sits at the TOP LEVEL of the document.
|
|
1165
|
+
const record = res;
|
|
1166
|
+
const accessToken = typeof record.access_token === 'string' ? record.access_token : undefined;
|
|
1167
|
+
const sessionId = typeof record.session_id === 'string' ? record.session_id : undefined;
|
|
1168
|
+
const deviceId = typeof record.deviceId === 'string' ? record.deviceId : undefined;
|
|
1169
|
+
const deviceSecret = typeof record.deviceSecret === 'string' ? record.deviceSecret : undefined;
|
|
1156
1170
|
const userRaw = record.user;
|
|
1157
1171
|
if (!sessionId || !deviceId || !deviceSecret || !userRaw || typeof userRaw !== 'object') {
|
|
1158
1172
|
throw new Error('auth/oauth/token returned an incomplete session payload');
|
|
1159
1173
|
}
|
|
1160
1174
|
const userObj = userRaw;
|
|
1161
|
-
const userId = userObj.id;
|
|
1175
|
+
const userId = typeof userObj.id === 'string' ? userObj.id : undefined;
|
|
1162
1176
|
if (!userId) {
|
|
1163
1177
|
throw new Error('auth/oauth/token returned a session without user.id');
|
|
1164
1178
|
}
|
|
1165
|
-
const expiresInSec = typeof record.expires_in === 'number'
|
|
1166
|
-
? record.expires_in
|
|
1167
|
-
: typeof record.expiresIn === 'number'
|
|
1168
|
-
? record.expiresIn
|
|
1169
|
-
: 15 * 60;
|
|
1179
|
+
const expiresInSec = typeof record.expires_in === 'number' ? record.expires_in : DEFAULT_ACCESS_TOKEN_TTL_SECONDS;
|
|
1170
1180
|
const expiresAt = new Date(Date.now() + expiresInSec * 1000).toISOString();
|
|
1171
1181
|
if (accessToken) {
|
|
1172
1182
|
this.setTokens(accessToken);
|
|
@@ -1188,5 +1198,33 @@ function OxyServicesAuthMixin(Base) {
|
|
|
1188
1198
|
throw this.handleError(error);
|
|
1189
1199
|
}
|
|
1190
1200
|
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Fetch OpenID Connect userinfo for the current bearer (`GET /auth/oauth/userinfo`).
|
|
1203
|
+
* The response is a flat JSON document — no `{ data }` wrapper.
|
|
1204
|
+
*/
|
|
1205
|
+
async getOAuthUserInfo() {
|
|
1206
|
+
try {
|
|
1207
|
+
const res = await this.makeRequest('GET', '/auth/oauth/userinfo', undefined, { cache: false });
|
|
1208
|
+
if (!res || typeof res !== 'object') {
|
|
1209
|
+
throw new Error('auth/oauth/userinfo returned an unexpected response shape');
|
|
1210
|
+
}
|
|
1211
|
+
const record = res;
|
|
1212
|
+
const sub = typeof record.sub === 'string' ? record.sub : undefined;
|
|
1213
|
+
if (!sub) {
|
|
1214
|
+
throw new Error('auth/oauth/userinfo returned a response without sub');
|
|
1215
|
+
}
|
|
1216
|
+
return {
|
|
1217
|
+
sub,
|
|
1218
|
+
...(typeof record.preferred_username === 'string'
|
|
1219
|
+
? { preferred_username: record.preferred_username }
|
|
1220
|
+
: {}),
|
|
1221
|
+
...(typeof record.name === 'string' ? { name: record.name } : {}),
|
|
1222
|
+
...(typeof record.picture === 'string' ? { picture: record.picture } : {}),
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
catch (error) {
|
|
1226
|
+
throw this.handleError(error);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1191
1229
|
};
|
|
1192
1230
|
}
|
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.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;
|
|
19
|
+
exports.isOfficialWebOrigin = exports.registrableApex = exports.publishOxyUserInvalidation = exports.evictOxyIdentityCache = exports.createOxyUserInvalidationHandler = 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; } });
|
|
@@ -55,6 +55,12 @@ Object.defineProperty(exports, "OXY_CSP_BASELINE", { enumerable: true, get: func
|
|
|
55
55
|
// Constant-time secret comparison.
|
|
56
56
|
var verifySecret_1 = require("./verifySecret");
|
|
57
57
|
Object.defineProperty(exports, "verifySecret", { enumerable: true, get: function () { return verifySecret_1.verifySecret; } });
|
|
58
|
+
// Cross-service user-invalidation signal: oxy-api publishes when identity
|
|
59
|
+
// changes, every consuming backend sweeps its caches instead of waiting out a TTL.
|
|
60
|
+
var userInvalidation_1 = require("./userInvalidation");
|
|
61
|
+
Object.defineProperty(exports, "createOxyUserInvalidationHandler", { enumerable: true, get: function () { return userInvalidation_1.createOxyUserInvalidationHandler; } });
|
|
62
|
+
Object.defineProperty(exports, "evictOxyIdentityCache", { enumerable: true, get: function () { return userInvalidation_1.evictOxyIdentityCache; } });
|
|
63
|
+
Object.defineProperty(exports, "publishOxyUserInvalidation", { enumerable: true, get: function () { return userInvalidation_1.publishOxyUserInvalidation; } });
|
|
58
64
|
// Registrable-apex (eTLD+1) derivation via the Public Suffix List — the SINGLE
|
|
59
65
|
// SOURCE OF TRUTH shared with the IdP worker and the client FAPI auto-detect.
|
|
60
66
|
// Pure host handling (no browser deps), so it is safe on the server subpath and
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Oxy user-invalidation publish/consume helpers for Oxy backends.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS
|
|
6
|
+
* ---------------
|
|
7
|
+
* Every Oxy backend caches Oxy identity, and none of them find out when it
|
|
8
|
+
* changes. The `OxyServices` GET response cache holds `GET /users/:id` and
|
|
9
|
+
* `GET /profiles/username/:name` for five minutes; it is swept when THIS process
|
|
10
|
+
* writes the profile (see the `clearCacheEntry` calls in the user mixin) and
|
|
11
|
+
* never when somebody else does — which is the normal case, since profiles are
|
|
12
|
+
* edited in Oxy's own apps. So an avatar or display-name change is invisible to
|
|
13
|
+
* every consuming backend for up to five minutes, per process.
|
|
14
|
+
*
|
|
15
|
+
* oxy-api broadcasts {@link OXY_USER_INVALIDATION_CHANNEL} on the shared Valkey
|
|
16
|
+
* when a user's identity changes. This module is the consumer half: it parses
|
|
17
|
+
* and validates the event, sweeps the SDK's own cache, and hands the event to
|
|
18
|
+
* app-specific eviction. Wiring it is two lines and every backend that does so
|
|
19
|
+
* stops serving stale identity.
|
|
20
|
+
*
|
|
21
|
+
* WHY THE TRANSPORT IS THE CALLER'S JOB
|
|
22
|
+
* -------------------------------------
|
|
23
|
+
* This module deliberately does NOT take a Redis client. ioredis and node-redis
|
|
24
|
+
* disagree about how to subscribe — node-redis passes the listener to
|
|
25
|
+
* `subscribe(channel, listener)`, ioredis takes `subscribe(channel)` and then
|
|
26
|
+
* emits `'message'` on the client — and a helper that accepted "a client" would
|
|
27
|
+
* have to sniff which library it was handed. That kind of detection is exactly
|
|
28
|
+
* what breaks silently when a consumer upgrades a client library.
|
|
29
|
+
*
|
|
30
|
+
* So the split is: this module owns parsing, validation, dispatch and the
|
|
31
|
+
* never-throw guarantee (the parts that are easy to get wrong and identical
|
|
32
|
+
* everywhere), and the caller owns its own client's two-line subscribe idiom
|
|
33
|
+
* (trivial, but library-specific).
|
|
34
|
+
*
|
|
35
|
+
* // node-redis
|
|
36
|
+
* await subscriber.subscribe(
|
|
37
|
+
* OXY_USER_INVALIDATION_CHANNEL,
|
|
38
|
+
* createOxyUserInvalidationHandler({ oxy: oxyClient }),
|
|
39
|
+
* );
|
|
40
|
+
*
|
|
41
|
+
* // ioredis
|
|
42
|
+
* const handle = createOxyUserInvalidationHandler({ oxy: oxyClient });
|
|
43
|
+
* await subscriber.subscribe(OXY_USER_INVALIDATION_CHANNEL);
|
|
44
|
+
* subscriber.on('message', (_channel, raw) => handle(raw));
|
|
45
|
+
*
|
|
46
|
+
* Subscribe on EVERY task, not just an elected leader. The SDK cache this sweeps
|
|
47
|
+
* is per-process in-memory, so a leader-only subscriber would leave every other
|
|
48
|
+
* task stale — and leader-gating would add a failure mode (leader down means no
|
|
49
|
+
* invalidation anywhere) to a signal whose whole point is that losing it is
|
|
50
|
+
* merely slow, never wrong.
|
|
51
|
+
*
|
|
52
|
+
* Node-only; exported solely from `@oxyhq/core/server`.
|
|
53
|
+
*/
|
|
54
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
+
exports.publishOxyUserInvalidation = publishOxyUserInvalidation;
|
|
56
|
+
exports.createOxyUserInvalidationHandler = createOxyUserInvalidationHandler;
|
|
57
|
+
exports.evictOxyIdentityCache = evictOxyIdentityCache;
|
|
58
|
+
const contracts_1 = require("@oxyhq/contracts");
|
|
59
|
+
/**
|
|
60
|
+
* Broadcast that an Oxy user's record changed.
|
|
61
|
+
*
|
|
62
|
+
* Returns `true` when a message was put on the wire and `false` when the reason
|
|
63
|
+
* is not a broadcast one ({@link isPublishedOxyUserChangeReason}) — the latter is
|
|
64
|
+
* a deliberate no-op, not a failure. Suppressing at the publisher rather than
|
|
65
|
+
* letting every subscriber discard matters at bulk-follow scale, where a single
|
|
66
|
+
* call moves up to 200 edges.
|
|
67
|
+
*
|
|
68
|
+
* NEVER THROWS AND NEVER RETURNS A REJECTED PROMISE. This is called from inside
|
|
69
|
+
* cache invalidation, which itself runs after a successful database write on the
|
|
70
|
+
* request path: a publish failure must not turn a completed profile update into
|
|
71
|
+
* a 500. A dropped message costs a consumer its TTL and nothing more.
|
|
72
|
+
*
|
|
73
|
+
* @param publisher - A connected Redis client. Must NOT be a client currently in
|
|
74
|
+
* subscriber mode — Redis forbids `PUBLISH` on a subscribed
|
|
75
|
+
* connection, so pass the publisher half of a pub/sub pair.
|
|
76
|
+
* @param userId - The Oxy user whose record changed.
|
|
77
|
+
* @param reason - How the record changed. See {@link OxyUserChangeReason}.
|
|
78
|
+
* @param onError - Optional diagnostic sink for a failed publish.
|
|
79
|
+
*/
|
|
80
|
+
function publishOxyUserInvalidation(publisher, userId, reason, onError) {
|
|
81
|
+
if (!userId || !(0, contracts_1.isPublishedOxyUserChangeReason)(reason)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const event = { userId, reason, at: Date.now() };
|
|
85
|
+
try {
|
|
86
|
+
const result = publisher.publish(contracts_1.OXY_USER_INVALIDATION_CHANNEL, JSON.stringify(event));
|
|
87
|
+
// node-redis returns a promise; ioredis returns a promise too, but a mocked
|
|
88
|
+
// or synchronous client may return anything. Only attach a rejection handler
|
|
89
|
+
// when there is actually something thenable to reject.
|
|
90
|
+
if (isPromiseLike(result)) {
|
|
91
|
+
Promise.resolve(result).catch((error) => onError?.(error));
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
onError?.(error);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function isPromiseLike(value) {
|
|
101
|
+
return (typeof value === 'object' &&
|
|
102
|
+
value !== null &&
|
|
103
|
+
typeof value.then === 'function');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build the message handler for {@link OXY_USER_INVALIDATION_CHANNEL}.
|
|
107
|
+
*
|
|
108
|
+
* The returned function NEVER THROWS and never returns a rejected promise. It
|
|
109
|
+
* runs inside the Redis client's message dispatch, where an exception either
|
|
110
|
+
* takes down the subscriber connection or surfaces as an unhandled rejection —
|
|
111
|
+
* and losing the subscription is strictly worse than losing one message, because
|
|
112
|
+
* it is silent and permanent.
|
|
113
|
+
*
|
|
114
|
+
* A message that fails schema validation is dropped, not retried: the payload is
|
|
115
|
+
* produced by a contract both sides compile against, so a malformed one means a
|
|
116
|
+
* version skew or an unrelated publisher on the channel, neither of which a retry
|
|
117
|
+
* fixes.
|
|
118
|
+
*/
|
|
119
|
+
function createOxyUserInvalidationHandler(options = {}) {
|
|
120
|
+
const { oxy, onInvalidate, onError } = options;
|
|
121
|
+
return (raw) => {
|
|
122
|
+
let event;
|
|
123
|
+
try {
|
|
124
|
+
const parsed = contracts_1.oxyUserInvalidationEventSchema.safeParse(JSON.parse(raw));
|
|
125
|
+
if (!parsed.success) {
|
|
126
|
+
onError?.(parsed.error, raw);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
event = parsed.data;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
onError?.(error, raw);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (oxy) {
|
|
136
|
+
try {
|
|
137
|
+
evictOxyIdentityCache(oxy, event.userId);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
// A cache sweep must never cost us the app-specific eviction below.
|
|
141
|
+
onError?.(error, raw);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!onInvalidate)
|
|
145
|
+
return;
|
|
146
|
+
try {
|
|
147
|
+
const result = onInvalidate(event);
|
|
148
|
+
if (isPromiseLike(result)) {
|
|
149
|
+
Promise.resolve(result).catch((error) => onError?.(error, raw));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
onError?.(error, raw);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Sweep an `OxyServices` GET response cache of everything that could carry the
|
|
159
|
+
* given user's identity.
|
|
160
|
+
*
|
|
161
|
+
* The by-id entry is exact. The by-username and resolve entries are keyed by
|
|
162
|
+
* HANDLE, which cannot be derived from an id without the very lookup we are
|
|
163
|
+
* invalidating, so those are swept by prefix — the same imprecision the SDK
|
|
164
|
+
* already accepts when it sweeps its own cache after a local profile write, and
|
|
165
|
+
* bounded by the fact that over-eviction costs a refetch and can never serve
|
|
166
|
+
* wrong data.
|
|
167
|
+
*/
|
|
168
|
+
function evictOxyIdentityCache(oxy, userId) {
|
|
169
|
+
oxy.clearCacheEntry(`GET:/users/${userId}`);
|
|
170
|
+
oxy.clearCacheByPrefix('GET:/profiles/username/');
|
|
171
|
+
oxy.clearCacheByPrefix('GET:/profiles/resolve');
|
|
172
|
+
}
|