From 460a65cf612f4c86af8038cafcc7e7ffe9eb8440 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 8 Nov 2025 05:18:58 +1100 Subject: [PATCH 01/50] feat: Allow option `publicServerURL` to be set dynamically as asynchronous function (#9803) --- spec/index.spec.js | 169 ++++++++++++++++++++++++++++++++++++- src/Config.js | 76 ++++++++++++++--- src/Options/Definitions.js | 3 +- src/Options/docs.js | 2 +- src/Options/index.js | 4 +- src/Routers/UsersRouter.js | 2 +- src/middlewares.js | 1 + types/Options/index.d.ts | 2 +- types/ParseServer.d.ts | 5 ++ 9 files changed, 247 insertions(+), 17 deletions(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index 5093a6ea25..afc1b5362e 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -363,7 +363,7 @@ describe('server', () => { it('should throw when getting invalid mount', done => { reconfigureServer({ publicServerURL: 'blabla:/some' }).catch(error => { - expect(error).toEqual('publicServerURL should be a valid HTTPS URL starting with https://'); + expect(error).toEqual('The option publicServerURL must be a valid URL starting with http:// or https://.'); done(); }); }); @@ -685,4 +685,171 @@ describe('server', () => { }) .catch(done.fail); }); + + describe('publicServerURL', () => { + it('should load publicServerURL', async () => { + await reconfigureServer({ + publicServerURL: () => 'https://example.com/1', + }); + + await new Parse.Object('TestObject').save(); + + const config = Config.get(Parse.applicationId); + expect(config.publicServerURL).toEqual('https://example.com/1'); + }); + + it('should load publicServerURL from Promise', async () => { + await reconfigureServer({ + publicServerURL: () => Promise.resolve('https://example.com/1'), + }); + + await new Parse.Object('TestObject').save(); + + const config = Config.get(Parse.applicationId); + expect(config.publicServerURL).toEqual('https://example.com/1'); + }); + + it('should handle publicServerURL function throwing error', async () => { + const errorMessage = 'Failed to get public server URL'; + await reconfigureServer({ + publicServerURL: () => { + throw new Error(errorMessage); + }, + }); + + // The error should occur when trying to save an object (which triggers loadKeys in middleware) + await expectAsync( + new Parse.Object('TestObject').save() + ).toBeRejected(); + }); + + it('should handle publicServerURL Promise rejection', async () => { + const errorMessage = 'Async fetch of public server URL failed'; + await reconfigureServer({ + publicServerURL: () => Promise.reject(new Error(errorMessage)), + }); + + // The error should occur when trying to save an object (which triggers loadKeys in middleware) + await expectAsync( + new Parse.Object('TestObject').save() + ).toBeRejected(); + }); + + it('executes publicServerURL function on every config access', async () => { + let counter = 0; + await reconfigureServer({ + publicServerURL: () => { + counter++; + return `https://example.com/${counter}`; + }, + }); + + // First request - should call the function + await new Parse.Object('TestObject').save(); + const config1 = Config.get(Parse.applicationId); + expect(config1.publicServerURL).toEqual('https://example.com/1'); + expect(counter).toEqual(1); + + // Second request - should call the function again + await new Parse.Object('TestObject').save(); + const config2 = Config.get(Parse.applicationId); + expect(config2.publicServerURL).toEqual('https://example.com/2'); + expect(counter).toEqual(2); + + // Third request - should call the function again + await new Parse.Object('TestObject').save(); + const config3 = Config.get(Parse.applicationId); + expect(config3.publicServerURL).toEqual('https://example.com/3'); + expect(counter).toEqual(3); + }); + + it('executes publicServerURL function on every password reset email', async () => { + let counter = 0; + const emailCalls = []; + + const emailAdapter = MockEmailAdapterWithOptions({ + sendPasswordResetEmail: ({ link }) => { + emailCalls.push(link); + return Promise.resolve(); + }, + }); + + await reconfigureServer({ + appName: 'test-app', + publicServerURL: () => { + counter++; + return `https://example.com/${counter}`; + }, + emailAdapter, + }); + + // Create a user + const user = new Parse.User(); + user.setUsername('user'); + user.setPassword('pass'); + user.setEmail('user@example.com'); + await user.signUp(); + + // Should use first publicServerURL + const counterBefore1 = counter; + await Parse.User.requestPasswordReset('user@example.com'); + await jasmine.timeout(); + expect(emailCalls.length).toEqual(1); + expect(emailCalls[0]).toContain(`https://example.com/${counterBefore1 + 1}`); + expect(counter).toBeGreaterThanOrEqual(2); + + // Should use updated publicServerURL + const counterBefore2 = counter; + await Parse.User.requestPasswordReset('user@example.com'); + await jasmine.timeout(); + expect(emailCalls.length).toEqual(2); + expect(emailCalls[1]).toContain(`https://example.com/${counterBefore2 + 1}`); + expect(counterBefore2).toBeGreaterThan(counterBefore1); + }); + + it('executes publicServerURL function on every verification email', async () => { + let counter = 0; + const emailCalls = []; + + const emailAdapter = MockEmailAdapterWithOptions({ + sendVerificationEmail: ({ link }) => { + emailCalls.push(link); + return Promise.resolve(); + }, + }); + + await reconfigureServer({ + appName: 'test-app', + verifyUserEmails: true, + publicServerURL: () => { + counter++; + return `https://example.com/${counter}`; + }, + emailAdapter, + }); + + // Should trigger verification email with first publicServerURL + const counterBefore1 = counter; + const user1 = new Parse.User(); + user1.setUsername('user1'); + user1.setPassword('pass1'); + user1.setEmail('user1@example.com'); + await user1.signUp(); + await jasmine.timeout(); + expect(emailCalls.length).toEqual(1); + expect(emailCalls[0]).toContain(`https://example.com/${counterBefore1 + 1}`); + + // Should trigger verification email with updated publicServerURL + const counterBefore2 = counter; + const user2 = new Parse.User(); + user2.setUsername('user2'); + user2.setPassword('pass2'); + user2.setEmail('user2@example.com'); + await user2.signUp(); + await jasmine.timeout(); + expect(emailCalls.length).toEqual(2); + expect(emailCalls[1]).toContain(`https://example.com/${counterBefore2 + 1}`); + expect(counterBefore2).toBeGreaterThan(counterBefore1); + }); + }); }); diff --git a/src/Config.js b/src/Config.js index bf6d50626c..42b24f2d89 100644 --- a/src/Config.js +++ b/src/Config.js @@ -32,6 +32,11 @@ function removeTrailingSlash(str) { return str; } +/** + * Config keys that need to be loaded asynchronously. + */ +const asyncKeys = ['publicServerURL']; + export class Config { static get(applicationId: string, mount: string) { const cacheInfo = AppCache.get(applicationId); @@ -56,9 +61,42 @@ export class Config { return config; } + async loadKeys() { + await Promise.all( + asyncKeys.map(async key => { + if (typeof this[`_${key}`] === 'function') { + try { + this[key] = await this[`_${key}`](); + } catch (error) { + throw new Error(`Failed to resolve async config key '${key}': ${error.message}`); + } + } + }) + ); + + const cachedConfig = AppCache.get(this.appId); + if (cachedConfig) { + const updatedConfig = { ...cachedConfig }; + asyncKeys.forEach(key => { + updatedConfig[key] = this[key]; + }); + AppCache.put(this.appId, updatedConfig); + } + } + + static transformConfiguration(serverConfiguration) { + for (const key of Object.keys(serverConfiguration)) { + if (asyncKeys.includes(key) && typeof serverConfiguration[key] === 'function') { + serverConfiguration[`_${key}`] = serverConfiguration[key]; + delete serverConfiguration[key]; + } + } + } + static put(serverConfiguration) { Config.validateOptions(serverConfiguration); Config.validateControllers(serverConfiguration); + Config.transformConfiguration(serverConfiguration); AppCache.put(serverConfiguration.appId, serverConfiguration); Config.setupPasswordValidator(serverConfiguration.passwordPolicy); return serverConfiguration; @@ -115,11 +153,7 @@ export class Config { throw 'extendSessionOnUse must be a boolean value'; } - if (publicServerURL) { - if (!publicServerURL.startsWith('http://') && !publicServerURL.startsWith('https://')) { - throw 'publicServerURL should be a valid HTTPS URL starting with https://'; - } - } + this.validatePublicServerURL({ publicServerURL }); this.validateSessionConfiguration(sessionLength, expireInactiveSessions); this.validateIps('masterKeyIps', masterKeyIps); this.validateIps('maintenanceKeyIps', maintenanceKeyIps); @@ -154,6 +188,7 @@ export class Config { userController, appName, publicServerURL, + _publicServerURL, emailVerifyTokenValidityDuration, emailVerifyTokenReuseIfValid, }) { @@ -162,7 +197,7 @@ export class Config { this.validateEmailConfiguration({ emailAdapter, appName, - publicServerURL, + publicServerURL: publicServerURL || _publicServerURL, emailVerifyTokenValidityDuration, emailVerifyTokenReuseIfValid, }); @@ -432,6 +467,30 @@ export class Config { } } + static validatePublicServerURL({ publicServerURL, required = false }) { + if (!publicServerURL) { + if (!required) { + return; + } + throw 'The option publicServerURL is required.'; + } + + const type = typeof publicServerURL; + + if (type === 'string') { + if (!publicServerURL.startsWith('http://') && !publicServerURL.startsWith('https://')) { + throw 'The option publicServerURL must be a valid URL starting with http:// or https://.'; + } + return; + } + + if (type === 'function') { + return; + } + + throw `The option publicServerURL must be a string or function, but got ${type}.`; + } + static validateEmailConfiguration({ emailAdapter, appName, @@ -445,9 +504,7 @@ export class Config { if (typeof appName !== 'string') { throw 'An app name is required for e-mail verification and password resets.'; } - if (typeof publicServerURL !== 'string') { - throw 'A public server url is required for e-mail verification and password resets.'; - } + this.validatePublicServerURL({ publicServerURL, required: true }); if (emailVerifyTokenValidityDuration) { if (isNaN(emailVerifyTokenValidityDuration)) { throw 'Email verify token validity duration must be a valid number.'; @@ -757,7 +814,6 @@ export class Config { return this.masterKey; } - // TODO: Remove this function once PagesRouter replaces the PublicAPIRouter; // the (default) endpoint has to be defined in PagesRouter only. get pagesEndpoint() { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index d5674eaf29..6c91b1c42c 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -495,7 +495,8 @@ module.exports.ParseServerOptions = { }, publicServerURL: { env: 'PARSE_PUBLIC_SERVER_URL', - help: 'Public URL to your parse server with http:// or https://.', + help: + 'Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`.', }, push: { env: 'PARSE_SERVER_PUSH', diff --git a/src/Options/docs.js b/src/Options/docs.js index 4d268847b1..cbe3efbf39 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -87,7 +87,7 @@ * @property {Boolean} preventLoginWithUnverifiedEmail Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required.

Default is `false`.
Requires option `verifyUserEmails: true`. * @property {Boolean} preventSignupWithUnverifiedEmail If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.

Default is `false`.
Requires option `verifyUserEmails: true`. * @property {ProtectedFields} protectedFields Protected fields that should be treated with extra security when fetching details. - * @property {String} publicServerURL Public URL to your parse server with http:// or https://. + * @property {Union} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`. * @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications * @property {RateLimitOptions[]} rateLimit Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

ℹ️ Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case. * @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes diff --git a/src/Options/index.js b/src/Options/index.js index d5317646ba..54195a8c52 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -226,9 +226,9 @@ export interface ParseServerOptions { /* If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`.

ℹ️ The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`. :DEFAULT: true */ encodeParseObjectInCloudFunction: ?boolean; - /* Public URL to your parse server with http:// or https://. + /* Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`. :ENV: PARSE_PUBLIC_SERVER_URL */ - publicServerURL: ?string; + publicServerURL: ?(string | (() => string) | (() => Promise)); /* The options for pages such as password reset and email verification. :DEFAULT: {} */ pages: ?PagesOptions; diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 7668562965..4f38c60b6c 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -418,7 +418,7 @@ export class UsersRouter extends ClassesRouter { Config.validateEmailConfiguration({ emailAdapter: req.config.userController.adapter, appName: req.config.appName, - publicServerURL: req.config.publicServerURL, + publicServerURL: req.config.publicServerURL || req.config._publicServerURL, emailVerifyTokenValidityDuration: req.config.emailVerifyTokenValidityDuration, emailVerifyTokenReuseIfValid: req.config.emailVerifyTokenReuseIfValid, }); diff --git a/src/middlewares.js b/src/middlewares.js index 6479987ba4..93b16f3846 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -213,6 +213,7 @@ export async function handleParseHeaders(req, res, next) { }); return; } + await config.loadKeys(); info.app = AppCache.get(info.appId); req.config = config; diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index 7a572a2f10..13fc1bd95d 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -85,7 +85,7 @@ export interface ParseServerOptions { cacheAdapter?: Adapter; emailAdapter?: Adapter; encodeParseObjectInCloudFunction?: boolean; - publicServerURL?: string; + publicServerURL?: string | (() => string) | (() => Promise); pages?: PagesOptions; customPages?: CustomPagesOptions; liveQuery?: LiveQueryOptions; diff --git a/types/ParseServer.d.ts b/types/ParseServer.d.ts index e504e03114..9570f0cf16 100644 --- a/types/ParseServer.d.ts +++ b/types/ParseServer.d.ts @@ -26,6 +26,11 @@ declare class ParseServer { * @returns {Promise} a promise that resolves when the server is stopped */ handleShutdown(): Promise; + /** + * @static + * Allow developers to customize each request with inversion of control/dependency injection + */ + static applyRequestContextMiddleware(api: any, options: any): void; /** * @static * Create an express app for the parse server From d5e7d6e0f618d35415b167c96ae8453b0d0de3fa Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 7 Nov 2025 18:19:51 +0000 Subject: [PATCH 02/50] chore(release): 8.5.0-alpha.1 [skip ci] # [8.5.0-alpha.1](https://github.com/parse-community/parse-server/compare/8.4.0...8.5.0-alpha.1) (2025-11-07) ### Features * Allow option `publicServerURL` to be set dynamically as asynchronous function ([#9803](https://github.com/parse-community/parse-server/issues/9803)) ([460a65c](https://github.com/parse-community/parse-server/commit/460a65cf612f4c86af8038cafcc7e7ffe9eb8440)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index bf414dc3f8..34638f1c35 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.1](https://github.com/parse-community/parse-server/compare/8.4.0...8.5.0-alpha.1) (2025-11-07) + + +### Features + +* Allow option `publicServerURL` to be set dynamically as asynchronous function ([#9803](https://github.com/parse-community/parse-server/issues/9803)) ([460a65c](https://github.com/parse-community/parse-server/commit/460a65cf612f4c86af8038cafcc7e7ffe9eb8440)) + # [8.4.0-alpha.2](https://github.com/parse-community/parse-server/compare/8.4.0-alpha.1...8.4.0-alpha.2) (2025-11-05) diff --git a/package-lock.json b/package-lock.json index 5c36e13703..d285a0260f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.4.0", + "version": "8.5.0-alpha.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.4.0", + "version": "8.5.0-alpha.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 4d8743c10a..a66e4cd89e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.4.0", + "version": "8.5.0-alpha.1", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 1b661e98c86a1db79e076a7297cd9199a72ae1ac Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:11:12 +0100 Subject: [PATCH 03/50] feat: Add support for MongoDB driver options `serverSelectionTimeoutMS`, `maxIdleTimeMS`, `heartbeatFrequencyMS` (#9910) --- spec/ParseConfigKey.spec.js | 3 +++ src/Options/Definitions.js | 18 ++++++++++++++++++ src/Options/docs.js | 3 +++ src/Options/index.js | 6 ++++++ types/Options/index.d.ts | 3 +++ 5 files changed, 33 insertions(+) diff --git a/spec/ParseConfigKey.spec.js b/spec/ParseConfigKey.spec.js index ae31ff954d..dcc51d5c7d 100644 --- a/spec/ParseConfigKey.spec.js +++ b/spec/ParseConfigKey.spec.js @@ -78,6 +78,9 @@ describe('Config Keys', () => { maxStalenessSeconds: 10, maxPoolSize: 10, minPoolSize: 5, + serverSelectionTimeoutMS: 5000, + maxIdleTimeMS: 60000, + heartbeatFrequencyMS: 10000, connectTimeoutMS: 5000, socketTimeoutMS: 5000, autoSelectFamily: true, diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 6c91b1c42c..74fc31a9bc 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1164,6 +1164,18 @@ module.exports.DatabaseOptions = { action: parsers.booleanParser, default: false, }, + heartbeatFrequencyMS: { + env: 'PARSE_SERVER_DATABASE_HEARTBEAT_FREQUENCY_MS', + help: + 'The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment.', + action: parsers.numberParser('heartbeatFrequencyMS'), + }, + maxIdleTimeMS: { + env: 'PARSE_SERVER_DATABASE_MAX_IDLE_TIME_MS', + help: + 'The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed.', + action: parsers.numberParser('maxIdleTimeMS'), + }, maxPoolSize: { env: 'PARSE_SERVER_DATABASE_MAX_POOL_SIZE', help: @@ -1199,6 +1211,12 @@ module.exports.DatabaseOptions = { 'The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires.', action: parsers.numberParser('schemaCacheTtl'), }, + serverSelectionTimeoutMS: { + env: 'PARSE_SERVER_DATABASE_SERVER_SELECTION_TIMEOUT_MS', + help: + 'The MongoDB driver option to specify the amount of time in milliseconds for a server to be considered suitable for selection.', + action: parsers.numberParser('serverSelectionTimeoutMS'), + }, socketTimeoutMS: { env: 'PARSE_SERVER_DATABASE_SOCKET_TIMEOUT_MS', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index cbe3efbf39..00b9634bb0 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -252,12 +252,15 @@ * @property {Boolean} createIndexUserUsernameCaseInsensitive Set to `true` to automatically create a case-insensitive index on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. * @property {Boolean} disableIndexFieldValidation Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. * @property {Boolean} enableSchemaHooks Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required. + * @property {Number} heartbeatFrequencyMS The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment. + * @property {Number} maxIdleTimeMS The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed. * @property {Number} maxPoolSize The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver. * @property {Number} maxStalenessSeconds The MongoDB driver option to set the maximum replication lag for reads from secondary nodes. * @property {Number} maxTimeMS The MongoDB driver option to set a cumulative time limit in milliseconds for processing operations on a cursor. * @property {Number} minPoolSize The MongoDB driver option to set the minimum number of opened, cached, ready-to-use database connections maintained by the driver. * @property {Boolean} retryWrites The MongoDB driver option to set whether to retry failed writes. * @property {Number} schemaCacheTtl The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires. + * @property {Number} serverSelectionTimeoutMS The MongoDB driver option to specify the amount of time in milliseconds for a server to be considered suitable for selection. * @property {Number} socketTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, spent attempting to send or receive on a socket before timing out. Specifying 0 means no timeout. */ diff --git a/src/Options/index.js b/src/Options/index.js index 54195a8c52..11908ae1cc 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -624,6 +624,12 @@ export interface DatabaseOptions { minPoolSize: ?number; /* The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver. */ maxPoolSize: ?number; + /* The MongoDB driver option to specify the amount of time in milliseconds for a server to be considered suitable for selection. */ + serverSelectionTimeoutMS: ?number; + /* The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed. */ + maxIdleTimeMS: ?number; + /* The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment. */ + heartbeatFrequencyMS: ?number; /* The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout. */ connectTimeoutMS: ?number; /* The MongoDB driver option to specify the amount of time, in milliseconds, spent attempting to send or receive on a socket before timing out. Specifying 0 means no timeout. */ diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index 13fc1bd95d..ee2e6afbe6 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -234,6 +234,9 @@ export interface DatabaseOptions { maxStalenessSeconds?: number; minPoolSize?: number; maxPoolSize?: number; + serverSelectionTimeoutMS?: number; + maxIdleTimeMS?: number; + heartbeatFrequencyMS?: number; connectTimeoutMS?: number; socketTimeoutMS?: number; autoSelectFamily?: boolean; From 412406915f2de14680459bc8d113bcf36fdb4d0d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 7 Nov 2025 19:12:11 +0000 Subject: [PATCH 04/50] chore(release): 8.5.0-alpha.2 [skip ci] # [8.5.0-alpha.2](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.1...8.5.0-alpha.2) (2025-11-07) ### Features * Add support for MongoDB driver options `serverSelectionTimeoutMS`, `maxIdleTimeMS`, `heartbeatFrequencyMS` ([#9910](https://github.com/parse-community/parse-server/issues/9910)) ([1b661e9](https://github.com/parse-community/parse-server/commit/1b661e98c86a1db79e076a7297cd9199a72ae1ac)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 34638f1c35..aca4ec7895 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.2](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.1...8.5.0-alpha.2) (2025-11-07) + + +### Features + +* Add support for MongoDB driver options `serverSelectionTimeoutMS`, `maxIdleTimeMS`, `heartbeatFrequencyMS` ([#9910](https://github.com/parse-community/parse-server/issues/9910)) ([1b661e9](https://github.com/parse-community/parse-server/commit/1b661e98c86a1db79e076a7297cd9199a72ae1ac)) + # [8.5.0-alpha.1](https://github.com/parse-community/parse-server/compare/8.4.0...8.5.0-alpha.1) (2025-11-07) diff --git a/package-lock.json b/package-lock.json index d285a0260f..2b31e644e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.1", + "version": "8.5.0-alpha.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.1", + "version": "8.5.0-alpha.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index a66e4cd89e..a2fa1c102b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.1", + "version": "8.5.0-alpha.2", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From cff451eabdc380affa600ed711de66f7bd1d00aa Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:41:59 +0100 Subject: [PATCH 05/50] feat: Add support for more MongoDB driver options (#9911) --- spec/ParseConfigKey.spec.js | 69 +++++++++++++-- src/Options/Definitions.js | 169 ++++++++++++++++++++++++++++++++++++ src/Options/docs.js | 32 +++++++ src/Options/index.js | 64 ++++++++++++++ types/Options/index.d.ts | 51 +++++++++-- 5 files changed, 368 insertions(+), 17 deletions(-) diff --git a/spec/ParseConfigKey.spec.js b/spec/ParseConfigKey.spec.js index dcc51d5c7d..2b6881e775 100644 --- a/spec/ParseConfigKey.spec.js +++ b/spec/ParseConfigKey.spec.js @@ -73,19 +73,70 @@ describe('Config Keys', () => { filesAdapter: null, databaseAdapter: null, databaseOptions: { - retryWrites: true, - maxTimeMS: 1000, - maxStalenessSeconds: 10, + appName: 'MyParseApp', + + // Cannot be tested as it requires authentication setup + // authMechanism: 'SCRAM-SHA-256', + // authMechanismProperties: { SERVICE_NAME: 'mongodb' }, + + authSource: 'admin', + autoSelectFamily: true, + autoSelectFamilyAttemptTimeout: 3000, + compressors: ['zlib'], + connectTimeoutMS: 5000, + directConnection: false, + disableIndexFieldValidation: true, + forceServerObjectId: false, + heartbeatFrequencyMS: 10000, + localThresholdMS: 15, + maxConnecting: 2, + maxIdleTimeMS: 60000, maxPoolSize: 10, + maxStalenessSeconds: 90, + maxTimeMS: 1000, minPoolSize: 5, + + // Cannot be tested as it requires a proxy setup + // proxyHost: 'proxy.example.com', + // proxyPassword: 'proxypass', + // proxyPort: 1080, + // proxyUsername: 'proxyuser', + + readConcernLevel: 'majority', + readPreference: 'secondaryPreferred', + readPreferenceTags: [{ dc: 'east' }], + + // Cannot be tested as it requires a replica set setup + // replicaSet: 'myReplicaSet', + + retryReads: true, + retryWrites: true, + serverMonitoringMode: 'auto', serverSelectionTimeoutMS: 5000, - maxIdleTimeMS: 60000, - heartbeatFrequencyMS: 10000, - connectTimeoutMS: 5000, socketTimeoutMS: 5000, - autoSelectFamily: true, - autoSelectFamilyAttemptTimeout: 3000, - disableIndexFieldValidation: true + + // Cannot be tested as it requires a replica cluster setup + // srvMaxHosts: 0, + // srvServiceName: 'mongodb', + + ssl: false, + tls: false, + tlsAllowInvalidCertificates: false, + tlsAllowInvalidHostnames: false, + tlsCAFile: __dirname + '/support/cert/cert.pem', + tlsCertificateKeyFile: __dirname + '/support/cert/cert.pem', + tlsCertificateKeyFilePassword: 'password', + waitQueueTimeoutMS: 5000, + zlibCompressionLevel: 6, + }, + })).toBeResolved(); + await expectAsync(reconfigureServer({ + databaseURI: 'mongodb://localhost:27017/parse', + filesAdapter: null, + databaseAdapter: null, + databaseOptions: { + // The following option needs to be tested separately due to driver config rules + tlsInsecure: false, }, })).toBeResolved(); expect(loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '')).not.toMatch(invalidKeyErrorMessage); diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 74fc31a9bc..96d1b9b32b 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1084,6 +1084,27 @@ module.exports.FileUploadOptions = { }, }; module.exports.DatabaseOptions = { + appName: { + env: 'PARSE_SERVER_DATABASE_APP_NAME', + help: + 'The MongoDB driver option to specify the name of the application that created this MongoClient instance.', + }, + authMechanism: { + env: 'PARSE_SERVER_DATABASE_AUTH_MECHANISM', + help: + 'The MongoDB driver option to specify the authentication mechanism that MongoDB will use to authenticate the connection.', + }, + authMechanismProperties: { + env: 'PARSE_SERVER_DATABASE_AUTH_MECHANISM_PROPERTIES', + help: + 'The MongoDB driver option to specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs.', + action: parsers.objectParser, + }, + authSource: { + env: 'PARSE_SERVER_DATABASE_AUTH_SOURCE', + help: + "The MongoDB driver option to specify the database name associated with the user's credentials.", + }, autoSelectFamily: { env: 'PARSE_SERVER_DATABASE_AUTO_SELECT_FAMILY', help: @@ -1096,6 +1117,11 @@ module.exports.DatabaseOptions = { 'The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead.', action: parsers.numberParser('autoSelectFamilyAttemptTimeout'), }, + compressors: { + env: 'PARSE_SERVER_DATABASE_COMPRESSORS', + help: + 'The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance.', + }, connectTimeoutMS: { env: 'PARSE_SERVER_DATABASE_CONNECT_TIMEOUT_MS', help: @@ -1151,6 +1177,12 @@ module.exports.DatabaseOptions = { action: parsers.booleanParser, default: true, }, + directConnection: { + env: 'PARSE_SERVER_DATABASE_DIRECT_CONNECTION', + help: + 'The MongoDB driver option to force a Single topology type with a connection string containing one host.', + action: parsers.booleanParser, + }, disableIndexFieldValidation: { env: 'PARSE_SERVER_DATABASE_DISABLE_INDEX_FIELD_VALIDATION', help: @@ -1164,12 +1196,35 @@ module.exports.DatabaseOptions = { action: parsers.booleanParser, default: false, }, + forceServerObjectId: { + env: 'PARSE_SERVER_DATABASE_FORCE_SERVER_OBJECT_ID', + help: 'The MongoDB driver option to force server to assign _id values instead of driver.', + action: parsers.booleanParser, + }, heartbeatFrequencyMS: { env: 'PARSE_SERVER_DATABASE_HEARTBEAT_FREQUENCY_MS', help: 'The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment.', action: parsers.numberParser('heartbeatFrequencyMS'), }, + loadBalanced: { + env: 'PARSE_SERVER_DATABASE_LOAD_BALANCED', + help: + 'The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service.', + action: parsers.booleanParser, + }, + localThresholdMS: { + env: 'PARSE_SERVER_DATABASE_LOCAL_THRESHOLD_MS', + help: + 'The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances.', + action: parsers.numberParser('localThresholdMS'), + }, + maxConnecting: { + env: 'PARSE_SERVER_DATABASE_MAX_CONNECTING', + help: + 'The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool.', + action: parsers.numberParser('maxConnecting'), + }, maxIdleTimeMS: { env: 'PARSE_SERVER_DATABASE_MAX_IDLE_TIME_MS', help: @@ -1200,6 +1255,51 @@ module.exports.DatabaseOptions = { 'The MongoDB driver option to set the minimum number of opened, cached, ready-to-use database connections maintained by the driver.', action: parsers.numberParser('minPoolSize'), }, + proxyHost: { + env: 'PARSE_SERVER_DATABASE_PROXY_HOST', + help: + 'The MongoDB driver option to configure a Socks5 proxy host used for creating TCP connections.', + }, + proxyPassword: { + env: 'PARSE_SERVER_DATABASE_PROXY_PASSWORD', + help: + 'The MongoDB driver option to configure a Socks5 proxy password when the proxy requires username/password authentication.', + }, + proxyPort: { + env: 'PARSE_SERVER_DATABASE_PROXY_PORT', + help: + 'The MongoDB driver option to configure a Socks5 proxy port used for creating TCP connections.', + action: parsers.numberParser('proxyPort'), + }, + proxyUsername: { + env: 'PARSE_SERVER_DATABASE_PROXY_USERNAME', + help: + 'The MongoDB driver option to configure a Socks5 proxy username when the proxy requires username/password authentication.', + }, + readConcernLevel: { + env: 'PARSE_SERVER_DATABASE_READ_CONCERN_LEVEL', + help: 'The MongoDB driver option to specify the level of isolation.', + }, + readPreference: { + env: 'PARSE_SERVER_DATABASE_READ_PREFERENCE', + help: 'The MongoDB driver option to specify the read preferences for this connection.', + }, + readPreferenceTags: { + env: 'PARSE_SERVER_DATABASE_READ_PREFERENCE_TAGS', + help: + 'The MongoDB driver option to specify the tags document as a comma-separated list of colon-separated key-value pairs.', + action: parsers.arrayParser, + }, + replicaSet: { + env: 'PARSE_SERVER_DATABASE_REPLICA_SET', + help: + 'The MongoDB driver option to specify the name of the replica set, if the mongod is a member of a replica set.', + }, + retryReads: { + env: 'PARSE_SERVER_DATABASE_RETRY_READS', + help: 'The MongoDB driver option to enable retryable reads.', + action: parsers.booleanParser, + }, retryWrites: { env: 'PARSE_SERVER_DATABASE_RETRY_WRITES', help: 'The MongoDB driver option to set whether to retry failed writes.', @@ -1211,6 +1311,11 @@ module.exports.DatabaseOptions = { 'The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires.', action: parsers.numberParser('schemaCacheTtl'), }, + serverMonitoringMode: { + env: 'PARSE_SERVER_DATABASE_SERVER_MONITORING_MODE', + help: + 'The MongoDB driver option to instruct the driver monitors to use a specific monitoring mode.', + }, serverSelectionTimeoutMS: { env: 'PARSE_SERVER_DATABASE_SERVER_SELECTION_TIMEOUT_MS', help: @@ -1223,6 +1328,70 @@ module.exports.DatabaseOptions = { 'The MongoDB driver option to specify the amount of time, in milliseconds, spent attempting to send or receive on a socket before timing out. Specifying 0 means no timeout.', action: parsers.numberParser('socketTimeoutMS'), }, + srvMaxHosts: { + env: 'PARSE_SERVER_DATABASE_SRV_MAX_HOSTS', + help: + 'The MongoDB driver option to specify the maximum number of hosts to connect to when using an srv connection string, a setting of 0 means unlimited hosts.', + action: parsers.numberParser('srvMaxHosts'), + }, + srvServiceName: { + env: 'PARSE_SERVER_DATABASE_SRV_SERVICE_NAME', + help: 'The MongoDB driver option to modify the srv URI service name.', + }, + ssl: { + env: 'PARSE_SERVER_DATABASE_SSL', + help: + 'The MongoDB driver option to enable or disable TLS/SSL for the connection (equivalent to tls option).', + action: parsers.booleanParser, + }, + tls: { + env: 'PARSE_SERVER_DATABASE_TLS', + help: 'The MongoDB driver option to enable or disable TLS/SSL for the connection.', + action: parsers.booleanParser, + }, + tlsAllowInvalidCertificates: { + env: 'PARSE_SERVER_DATABASE_TLS_ALLOW_INVALID_CERTIFICATES', + help: + 'The MongoDB driver option to bypass validation of the certificates presented by the mongod/mongos instance.', + action: parsers.booleanParser, + }, + tlsAllowInvalidHostnames: { + env: 'PARSE_SERVER_DATABASE_TLS_ALLOW_INVALID_HOSTNAMES', + help: + 'The MongoDB driver option to disable hostname validation of the certificate presented by the mongod/mongos instance.', + action: parsers.booleanParser, + }, + tlsCAFile: { + env: 'PARSE_SERVER_DATABASE_TLS_CAFILE', + help: + 'The MongoDB driver option to specify the location of a local .pem file that contains the root certificate chain from the Certificate Authority.', + }, + tlsCertificateKeyFile: { + env: 'PARSE_SERVER_DATABASE_TLS_CERTIFICATE_KEY_FILE', + help: + "The MongoDB driver option to specify the location of a local .pem file that contains the client's TLS/SSL certificate and key.", + }, + tlsCertificateKeyFilePassword: { + env: 'PARSE_SERVER_DATABASE_TLS_CERTIFICATE_KEY_FILE_PASSWORD', + help: 'The MongoDB driver option to specify the password to decrypt the tlsCertificateKeyFile.', + }, + tlsInsecure: { + env: 'PARSE_SERVER_DATABASE_TLS_INSECURE', + help: 'The MongoDB driver option to disable various certificate validations.', + action: parsers.booleanParser, + }, + waitQueueTimeoutMS: { + env: 'PARSE_SERVER_DATABASE_WAIT_QUEUE_TIMEOUT_MS', + help: + 'The MongoDB driver option to specify the maximum time in milliseconds that a thread can wait for a connection to become available.', + action: parsers.numberParser('waitQueueTimeoutMS'), + }, + zlibCompressionLevel: { + env: 'PARSE_SERVER_DATABASE_ZLIB_COMPRESSION_LEVEL', + help: + 'The MongoDB driver option to specify the compression level if using zlib for network compression (0-9).', + action: parsers.numberParser('zlibCompressionLevel'), + }, }; module.exports.AuthAdapter = { enabled: { diff --git a/src/Options/docs.js b/src/Options/docs.js index 00b9634bb0..ade153cdb3 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -240,8 +240,13 @@ /** * @interface DatabaseOptions + * @property {String} appName The MongoDB driver option to specify the name of the application that created this MongoClient instance. + * @property {String} authMechanism The MongoDB driver option to specify the authentication mechanism that MongoDB will use to authenticate the connection. + * @property {Any} authMechanismProperties The MongoDB driver option to specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs. + * @property {String} authSource The MongoDB driver option to specify the database name associated with the user's credentials. * @property {Boolean} autoSelectFamily The MongoDB driver option to set whether the socket attempts to connect to IPv6 and IPv4 addresses until a connection is established. If available, the driver will select the first IPv6 address. * @property {Number} autoSelectFamilyAttemptTimeout The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead. + * @property {Union} compressors The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance. * @property {Number} connectTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout. * @property {Boolean} createIndexRoleName Set to `true` to automatically create a unique index on the name field of the _Role collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. * @property {Boolean} createIndexUserEmail Set to `true` to automatically create indexes on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. @@ -250,18 +255,45 @@ * @property {Boolean} createIndexUserPasswordResetToken Set to `true` to automatically create an index on the _perishable_token field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. * @property {Boolean} createIndexUserUsername Set to `true` to automatically create indexes on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. * @property {Boolean} createIndexUserUsernameCaseInsensitive Set to `true` to automatically create a case-insensitive index on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. + * @property {Boolean} directConnection The MongoDB driver option to force a Single topology type with a connection string containing one host. * @property {Boolean} disableIndexFieldValidation Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. * @property {Boolean} enableSchemaHooks Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required. + * @property {Boolean} forceServerObjectId The MongoDB driver option to force server to assign _id values instead of driver. * @property {Number} heartbeatFrequencyMS The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment. + * @property {Boolean} loadBalanced The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service. + * @property {Number} localThresholdMS The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances. + * @property {Number} maxConnecting The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool. * @property {Number} maxIdleTimeMS The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed. * @property {Number} maxPoolSize The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver. * @property {Number} maxStalenessSeconds The MongoDB driver option to set the maximum replication lag for reads from secondary nodes. * @property {Number} maxTimeMS The MongoDB driver option to set a cumulative time limit in milliseconds for processing operations on a cursor. * @property {Number} minPoolSize The MongoDB driver option to set the minimum number of opened, cached, ready-to-use database connections maintained by the driver. + * @property {String} proxyHost The MongoDB driver option to configure a Socks5 proxy host used for creating TCP connections. + * @property {String} proxyPassword The MongoDB driver option to configure a Socks5 proxy password when the proxy requires username/password authentication. + * @property {Number} proxyPort The MongoDB driver option to configure a Socks5 proxy port used for creating TCP connections. + * @property {String} proxyUsername The MongoDB driver option to configure a Socks5 proxy username when the proxy requires username/password authentication. + * @property {String} readConcernLevel The MongoDB driver option to specify the level of isolation. + * @property {String} readPreference The MongoDB driver option to specify the read preferences for this connection. + * @property {Any[]} readPreferenceTags The MongoDB driver option to specify the tags document as a comma-separated list of colon-separated key-value pairs. + * @property {String} replicaSet The MongoDB driver option to specify the name of the replica set, if the mongod is a member of a replica set. + * @property {Boolean} retryReads The MongoDB driver option to enable retryable reads. * @property {Boolean} retryWrites The MongoDB driver option to set whether to retry failed writes. * @property {Number} schemaCacheTtl The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires. + * @property {String} serverMonitoringMode The MongoDB driver option to instruct the driver monitors to use a specific monitoring mode. * @property {Number} serverSelectionTimeoutMS The MongoDB driver option to specify the amount of time in milliseconds for a server to be considered suitable for selection. * @property {Number} socketTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, spent attempting to send or receive on a socket before timing out. Specifying 0 means no timeout. + * @property {Number} srvMaxHosts The MongoDB driver option to specify the maximum number of hosts to connect to when using an srv connection string, a setting of 0 means unlimited hosts. + * @property {String} srvServiceName The MongoDB driver option to modify the srv URI service name. + * @property {Boolean} ssl The MongoDB driver option to enable or disable TLS/SSL for the connection (equivalent to tls option). + * @property {Boolean} tls The MongoDB driver option to enable or disable TLS/SSL for the connection. + * @property {Boolean} tlsAllowInvalidCertificates The MongoDB driver option to bypass validation of the certificates presented by the mongod/mongos instance. + * @property {Boolean} tlsAllowInvalidHostnames The MongoDB driver option to disable hostname validation of the certificate presented by the mongod/mongos instance. + * @property {String} tlsCAFile The MongoDB driver option to specify the location of a local .pem file that contains the root certificate chain from the Certificate Authority. + * @property {String} tlsCertificateKeyFile The MongoDB driver option to specify the location of a local .pem file that contains the client's TLS/SSL certificate and key. + * @property {String} tlsCertificateKeyFilePassword The MongoDB driver option to specify the password to decrypt the tlsCertificateKeyFile. + * @property {Boolean} tlsInsecure The MongoDB driver option to disable various certificate validations. + * @property {Number} waitQueueTimeoutMS The MongoDB driver option to specify the maximum time in milliseconds that a thread can wait for a connection to become available. + * @property {Number} zlibCompressionLevel The MongoDB driver option to specify the compression level if using zlib for network compression (0-9). */ /** diff --git a/src/Options/index.js b/src/Options/index.js index 11908ae1cc..35b5821987 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -638,6 +638,70 @@ export interface DatabaseOptions { autoSelectFamily: ?boolean; /* The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead. */ autoSelectFamilyAttemptTimeout: ?number; + /* The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool. */ + maxConnecting: ?number; + /* The MongoDB driver option to specify the maximum time in milliseconds that a thread can wait for a connection to become available. */ + waitQueueTimeoutMS: ?number; + /* The MongoDB driver option to specify the name of the replica set, if the mongod is a member of a replica set. */ + replicaSet: ?string; + /* The MongoDB driver option to force a Single topology type with a connection string containing one host. */ + directConnection: ?boolean; + /* The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service. */ + loadBalanced: ?boolean; + /* The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances. */ + localThresholdMS: ?number; + /* The MongoDB driver option to specify the maximum number of hosts to connect to when using an srv connection string, a setting of 0 means unlimited hosts. */ + srvMaxHosts: ?number; + /* The MongoDB driver option to modify the srv URI service name. */ + srvServiceName: ?string; + /* The MongoDB driver option to enable or disable TLS/SSL for the connection. */ + tls: ?boolean; + /* The MongoDB driver option to enable or disable TLS/SSL for the connection (equivalent to tls option). */ + ssl: ?boolean; + /* The MongoDB driver option to specify the location of a local .pem file that contains the client's TLS/SSL certificate and key. */ + tlsCertificateKeyFile: ?string; + /* The MongoDB driver option to specify the password to decrypt the tlsCertificateKeyFile. */ + tlsCertificateKeyFilePassword: ?string; + /* The MongoDB driver option to specify the location of a local .pem file that contains the root certificate chain from the Certificate Authority. */ + tlsCAFile: ?string; + /* The MongoDB driver option to bypass validation of the certificates presented by the mongod/mongos instance. */ + tlsAllowInvalidCertificates: ?boolean; + /* The MongoDB driver option to disable hostname validation of the certificate presented by the mongod/mongos instance. */ + tlsAllowInvalidHostnames: ?boolean; + /* The MongoDB driver option to disable various certificate validations. */ + tlsInsecure: ?boolean; + /* The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance. */ + compressors: ?(string[] | string); + /* The MongoDB driver option to specify the compression level if using zlib for network compression (0-9). */ + zlibCompressionLevel: ?number; + /* The MongoDB driver option to specify the read preferences for this connection. */ + readPreference: ?string; + /* The MongoDB driver option to specify the tags document as a comma-separated list of colon-separated key-value pairs. */ + readPreferenceTags: ?(any[]); + /* The MongoDB driver option to specify the level of isolation. */ + readConcernLevel: ?string; + /* The MongoDB driver option to specify the database name associated with the user's credentials. */ + authSource: ?string; + /* The MongoDB driver option to specify the authentication mechanism that MongoDB will use to authenticate the connection. */ + authMechanism: ?string; + /* The MongoDB driver option to specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs. */ + authMechanismProperties: ?any; + /* The MongoDB driver option to specify the name of the application that created this MongoClient instance. */ + appName: ?string; + /* The MongoDB driver option to enable retryable reads. */ + retryReads: ?boolean; + /* The MongoDB driver option to force server to assign _id values instead of driver. */ + forceServerObjectId: ?boolean; + /* The MongoDB driver option to instruct the driver monitors to use a specific monitoring mode. */ + serverMonitoringMode: ?string; + /* The MongoDB driver option to configure a Socks5 proxy host used for creating TCP connections. */ + proxyHost: ?string; + /* The MongoDB driver option to configure a Socks5 proxy port used for creating TCP connections. */ + proxyPort: ?number; + /* The MongoDB driver option to configure a Socks5 proxy username when the proxy requires username/password authentication. */ + proxyUsername: ?string; + /* The MongoDB driver option to configure a Socks5 proxy password when the proxy requires username/password authentication. */ + proxyPassword: ?string; /* Set to `true` to automatically create indexes on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. :DEFAULT: true */ createIndexUserEmail: ?boolean; diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index ee2e6afbe6..3cb604bbe1 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -227,20 +227,55 @@ export interface FileUploadOptions { enableForPublic?: boolean; } export interface DatabaseOptions { + // Parse Server custom options enableSchemaHooks?: boolean; schemaCacheTtl?: number; - retryWrites?: boolean; - maxTimeMS?: number; + + // MongoDB driver options + appName?: string; + authMechanism?: string; + authMechanismProperties?: any; + authSource?: string; + autoSelectFamily?: boolean; + autoSelectFamilyAttemptTimeout?: number; + compressors?: string[] | string; + connectTimeoutMS?: number; + directConnection?: boolean; + forceServerObjectId?: boolean; + heartbeatFrequencyMS?: number; + loadBalanced?: boolean; + localThresholdMS?: number; + maxConnecting?: number; + maxIdleTimeMS?: number; + maxPoolSize?: number; maxStalenessSeconds?: number; + maxTimeMS?: number; minPoolSize?: number; - maxPoolSize?: number; + proxyHost?: string; + proxyPassword?: string; + proxyPort?: number; + proxyUsername?: string; + readConcernLevel?: string; + readPreference?: string; + readPreferenceTags?: any[]; + replicaSet?: string; + retryReads?: boolean; + retryWrites?: boolean; + serverMonitoringMode?: string; serverSelectionTimeoutMS?: number; - maxIdleTimeMS?: number; - heartbeatFrequencyMS?: number; - connectTimeoutMS?: number; socketTimeoutMS?: number; - autoSelectFamily?: boolean; - autoSelectFamilyAttemptTimeout?: number; + srvMaxHosts?: number; + srvServiceName?: string; + ssl?: boolean; + tls?: boolean; + tlsAllowInvalidCertificates?: boolean; + tlsAllowInvalidHostnames?: boolean; + tlsCAFile?: string; + tlsCertificateKeyFile?: string; + tlsCertificateKeyFilePassword?: string; + tlsInsecure?: boolean; + waitQueueTimeoutMS?: number; + zlibCompressionLevel?: number; } export interface AuthAdapter { enabled?: boolean; From 2424054221ddca4831d1d6bfb9157b3d6ab43e9a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 7 Nov 2025 20:42:49 +0000 Subject: [PATCH 06/50] chore(release): 8.5.0-alpha.3 [skip ci] # [8.5.0-alpha.3](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.2...8.5.0-alpha.3) (2025-11-07) ### Features * Add support for more MongoDB driver options ([#9911](https://github.com/parse-community/parse-server/issues/9911)) ([cff451e](https://github.com/parse-community/parse-server/commit/cff451eabdc380affa600ed711de66f7bd1d00aa)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index aca4ec7895..564e64d2e6 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.3](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.2...8.5.0-alpha.3) (2025-11-07) + + +### Features + +* Add support for more MongoDB driver options ([#9911](https://github.com/parse-community/parse-server/issues/9911)) ([cff451e](https://github.com/parse-community/parse-server/commit/cff451eabdc380affa600ed711de66f7bd1d00aa)) + # [8.5.0-alpha.2](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.1...8.5.0-alpha.2) (2025-11-07) diff --git a/package-lock.json b/package-lock.json index 2b31e644e6..4c9268211e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.2", + "version": "8.5.0-alpha.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.2", + "version": "8.5.0-alpha.3", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index a2fa1c102b..7c248f9505 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.2", + "version": "8.5.0-alpha.3", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From b760733b98bcfc9c09ac9780066602e1fda108fe Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:48:29 +0100 Subject: [PATCH 07/50] feat: Add MongoDB client event logging via database option `logClientEvents` (#9914) --- jsdoc-conf.json | 2 +- resources/buildConfigDefinitions.js | 8 +- spec/MongoStorageAdapter.spec.js | 239 ++++++++++++++++++ spec/Utils.spec.js | 65 +++++ .../Storage/Mongo/MongoStorageAdapter.js | 41 ++- src/Options/Definitions.js | 71 +++++- src/Options/docs.js | 28 +- src/Options/index.js | 38 ++- src/Utils.js | 34 +++ 9 files changed, 501 insertions(+), 25 deletions(-) diff --git a/jsdoc-conf.json b/jsdoc-conf.json index b410d239b0..e90f82556b 100644 --- a/jsdoc-conf.json +++ b/jsdoc-conf.json @@ -30,7 +30,7 @@ "theme_opts": { "default_theme": "dark", "title": "", - "create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }" + "create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px; height: auto; max-width: 100%; object-fit: contain; }" } }, "markdown": { diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 5b9084f863..f4813975f4 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -36,15 +36,17 @@ const nestedOptionEnvPrefix = { IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_', LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_', LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_', + LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_', + LogLevel: 'PARSE_SERVER_LOG_LEVEL_', + LogLevels: 'PARSE_SERVER_LOG_LEVELS_', PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_', PagesOptions: 'PARSE_SERVER_PAGES_', PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_', ParseServerOptions: 'PARSE_SERVER_', PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_', - SecurityOptions: 'PARSE_SERVER_SECURITY_', - SchemaOptions: 'PARSE_SERVER_SCHEMA_', - LogLevels: 'PARSE_SERVER_LOG_LEVELS_', RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_', + SchemaOptions: 'PARSE_SERVER_SCHEMA_', + SecurityOptions: 'PARSE_SERVER_SECURITY_', }; function last(array) { diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index 7d0d220cff..8b14973243 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -824,4 +824,243 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { expect(roleIndexes.find(idx => idx.name === 'name_1')).toBeDefined(); }); }); + + describe('logClientEvents', () => { + it('should log MongoDB client events when configured', async () => { + const logger = require('../lib/logger').logger; + const logSpy = spyOn(logger, 'warn'); + + const logClientEvents = [ + { + name: 'serverDescriptionChanged', + keys: ['address'], + logLevel: 'warn', + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { logClientEvents }, + }); + + // Connect to trigger event listeners setup + await adapter.connect(); + + // Manually trigger the event to test the listener + const mockEvent = { + address: 'localhost:27017', + previousDescription: { type: 'Unknown' }, + newDescription: { type: 'Standalone' }, + }; + + adapter.client.emit('serverDescriptionChanged', mockEvent); + + // Verify the log was called with the correct message + expect(logSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event serverDescriptionChanged:.*"address":"localhost:27017"/) + ); + + await adapter.handleShutdown(); + }); + + it('should log entire event when keys are not specified', async () => { + const logger = require('../lib/logger').logger; + const logSpy = spyOn(logger, 'info'); + + const logClientEvents = [ + { + name: 'connectionPoolReady', + logLevel: 'info', + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { logClientEvents }, + }); + + await adapter.connect(); + + const mockEvent = { + address: 'localhost:27017', + options: { maxPoolSize: 100 }, + }; + + adapter.client.emit('connectionPoolReady', mockEvent); + + expect(logSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017".*"options"/) + ); + + await adapter.handleShutdown(); + }); + + it('should extract nested keys using dot notation', async () => { + const logger = require('../lib/logger').logger; + const logSpy = spyOn(logger, 'warn'); + + const logClientEvents = [ + { + name: 'topologyDescriptionChanged', + keys: ['previousDescription.type', 'newDescription.type', 'newDescription.servers.size'], + logLevel: 'warn', + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { logClientEvents }, + }); + + await adapter.connect(); + + const mockEvent = { + topologyId: 1, + previousDescription: { type: 'Unknown' }, + newDescription: { + type: 'ReplicaSetWithPrimary', + servers: { size: 3 }, + }, + }; + + adapter.client.emit('topologyDescriptionChanged', mockEvent); + + expect(logSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event topologyDescriptionChanged:.*"previousDescription.type":"Unknown".*"newDescription.type":"ReplicaSetWithPrimary".*"newDescription.servers.size":3/) + ); + + await adapter.handleShutdown(); + }); + + it('should handle invalid log level gracefully', async () => { + const logger = require('../lib/logger').logger; + const infoSpy = spyOn(logger, 'info'); + + const logClientEvents = [ + { + name: 'connectionPoolReady', + keys: ['address'], + logLevel: 'invalidLogLevel', // Invalid log level + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { logClientEvents }, + }); + + await adapter.connect(); + + const mockEvent = { + address: 'localhost:27017', + }; + + adapter.client.emit('connectionPoolReady', mockEvent); + + // Should fallback to 'info' level + expect(infoSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017"/) + ); + + await adapter.handleShutdown(); + }); + + it('should handle Map and Set instances in events', async () => { + const logger = require('../lib/logger').logger; + const warnSpy = spyOn(logger, 'warn'); + + const logClientEvents = [ + { + name: 'customEvent', + logLevel: 'warn', + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { logClientEvents }, + }); + + await adapter.connect(); + + const mockEvent = { + mapData: new Map([['key1', 'value1'], ['key2', 'value2']]), + setData: new Set([1, 2, 3]), + }; + + adapter.client.emit('customEvent', mockEvent); + + // Should serialize Map and Set properly + expect(warnSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event customEvent:.*"mapData":\{"key1":"value1","key2":"value2"\}.*"setData":\[1,2,3\]/) + ); + + await adapter.handleShutdown(); + }); + + it('should handle missing keys in event object', async () => { + const logger = require('../lib/logger').logger; + const infoSpy = spyOn(logger, 'info'); + + const logClientEvents = [ + { + name: 'testEvent', + keys: ['nonexistent.nested.key', 'another.missing'], + logLevel: 'info', + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { logClientEvents }, + }); + + await adapter.connect(); + + const mockEvent = { + actualField: 'value', + }; + + adapter.client.emit('testEvent', mockEvent); + + // Should handle missing keys gracefully with undefined values + expect(infoSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event testEvent:/) + ); + + await adapter.handleShutdown(); + }); + + it('should handle circular references gracefully', async () => { + const logger = require('../lib/logger').logger; + const infoSpy = spyOn(logger, 'info'); + + const logClientEvents = [ + { + name: 'circularEvent', + logLevel: 'info', + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { logClientEvents }, + }); + + await adapter.connect(); + + // Create circular reference + const mockEvent = { name: 'test' }; + mockEvent.self = mockEvent; + + adapter.client.emit('circularEvent', mockEvent); + + // Should handle circular reference with [Circular] marker + expect(infoSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event circularEvent:.*\[Circular\]/) + ); + + await adapter.handleShutdown(); + }); + }); }); diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js index 14747af6aa..b1277c6bfe 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -57,4 +57,69 @@ describe('Utils', () => { }); }); }); + + describe('getCircularReplacer', () => { + it('should handle Map instances', () => { + const obj = { + name: 'test', + mapData: new Map([ + ['key1', 'value1'], + ['key2', 'value2'] + ]) + }; + const result = JSON.stringify(obj, Utils.getCircularReplacer()); + expect(result).toBe('{"name":"test","mapData":{"key1":"value1","key2":"value2"}}'); + }); + + it('should handle Set instances', () => { + const obj = { + name: 'test', + setData: new Set([1, 2, 3]) + }; + const result = JSON.stringify(obj, Utils.getCircularReplacer()); + expect(result).toBe('{"name":"test","setData":[1,2,3]}'); + }); + + it('should handle circular references', () => { + const obj = { name: 'test', value: 123 }; + obj.self = obj; + const result = JSON.stringify(obj, Utils.getCircularReplacer()); + expect(result).toBe('{"name":"test","value":123,"self":"[Circular]"}'); + }); + + it('should handle nested circular references', () => { + const obj = { + name: 'parent', + child: { + name: 'child' + } + }; + obj.child.parent = obj; + const result = JSON.stringify(obj, Utils.getCircularReplacer()); + expect(result).toBe('{"name":"parent","child":{"name":"child","parent":"[Circular]"}}'); + }); + + it('should handle mixed Map, Set, and circular references', () => { + const obj = { + mapData: new Map([['key', 'value']]), + setData: new Set([1, 2]), + regular: 'data' + }; + obj.circular = obj; + const result = JSON.stringify(obj, Utils.getCircularReplacer()); + expect(result).toBe('{"mapData":{"key":"value"},"setData":[1,2],"regular":"data","circular":"[Circular]"}'); + }); + + it('should handle normal objects without modification', () => { + const obj = { + name: 'test', + number: 42, + nested: { + key: 'value' + } + }; + const result = JSON.stringify(obj, Utils.getCircularReplacer()); + expect(result).toBe('{"name":"test","number":42,"nested":{"key":"value"}}'); + }); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 39b335d52e..57f7543085 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -1,16 +1,16 @@ // @flow +import { format as formatUrl, parse as parseUrl } from '../../../vendor/mongodbUrl'; +import type { QueryOptions, QueryType, SchemaType, StorageClass } from '../StorageAdapter'; +import { StorageAdapter } from '../StorageAdapter'; import MongoCollection from './MongoCollection'; import MongoSchemaCollection from './MongoSchemaCollection'; -import { StorageAdapter } from '../StorageAdapter'; -import type { SchemaType, QueryType, StorageClass, QueryOptions } from '../StorageAdapter'; -import { parse as parseUrl, format as formatUrl } from '../../../vendor/mongodbUrl'; import { - parseObjectToMongoObjectForCreate, mongoObjectToParseObject, + parseObjectToMongoObjectForCreate, transformKey, - transformWhere, - transformUpdate, transformPointerString, + transformUpdate, + transformWhere, } from './MongoTransform'; // @flow-disable-next import Parse from 'parse/node'; @@ -18,6 +18,7 @@ import Parse from 'parse/node'; import _ from 'lodash'; import defaults from '../../../defaults'; import logger from '../../../logger'; +import Utils from '../../../Utils'; // @flow-disable-next const mongodb = require('mongodb'); @@ -132,6 +133,7 @@ export class MongoStorageAdapter implements StorageAdapter { _mongoOptions: Object; _onchange: any; _stream: any; + _logClientEvents: ?Array; // Public connectionPromise: ?Promise; database: any; @@ -154,6 +156,7 @@ export class MongoStorageAdapter implements StorageAdapter { this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks; this.schemaCacheTtl = mongoOptions.schemaCacheTtl; this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation; + this._logClientEvents = mongoOptions.logClientEvents; // Remove Parse Server-specific options that should not be passed to MongoDB client // Note: We only delete from this._mongoOptions, not from the original mongoOptions object, // because other components (like DatabaseController) need access to these options @@ -162,6 +165,7 @@ export class MongoStorageAdapter implements StorageAdapter { 'schemaCacheTtl', 'maxTimeMS', 'disableIndexFieldValidation', + 'logClientEvents', 'createIndexUserUsername', 'createIndexUserUsernameCaseInsensitive', 'createIndexUserEmail', @@ -203,6 +207,31 @@ export class MongoStorageAdapter implements StorageAdapter { client.on('close', () => { delete this.connectionPromise; }); + + // Set up client event logging if configured + if (this._logClientEvents && Array.isArray(this._logClientEvents)) { + this._logClientEvents.forEach(eventConfig => { + client.on(eventConfig.name, event => { + let logData = {}; + if (!eventConfig.keys || eventConfig.keys.length === 0) { + logData = event; + } else { + eventConfig.keys.forEach(keyPath => { + logData[keyPath] = _.get(event, keyPath); + }); + } + + // Validate log level exists, fallback to 'info' + const logLevel = typeof logger[eventConfig.logLevel] === 'function' ? eventConfig.logLevel : 'info'; + + // Safe JSON serialization with Map/Set and circular reference support + const logMessage = `MongoDB client event ${eventConfig.name}: ${JSON.stringify(logData, Utils.getCircularReplacer())}`; + + logger[logLevel](logMessage); + }); + }); + } + this.client = client; this.database = database; }) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 96d1b9b32b..6930020de7 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1083,6 +1083,59 @@ module.exports.FileUploadOptions = { default: ['^(?![xXsS]?[hH][tT][mM][lL]?$)'], }, }; +/* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'silly'` - Silly level (lowest priority) */ +module.exports.LogLevel = { + debug: { + env: 'PARSE_SERVER_LOG_LEVEL_DEBUG', + help: 'Debug level', + required: true, + }, + error: { + env: 'PARSE_SERVER_LOG_LEVEL_ERROR', + help: 'Error level - highest priority', + required: true, + }, + info: { + env: 'PARSE_SERVER_LOG_LEVEL_INFO', + help: 'Info level - default', + required: true, + }, + silly: { + env: 'PARSE_SERVER_LOG_LEVEL_SILLY', + help: 'Silly level - lowest priority', + required: true, + }, + verbose: { + env: 'PARSE_SERVER_LOG_LEVEL_VERBOSE', + help: 'Verbose level', + required: true, + }, + warn: { + env: 'PARSE_SERVER_LOG_LEVEL_WARN', + help: 'Warning level', + required: true, + }, +}; +module.exports.LogClientEvent = { + keys: { + env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_KEYS', + help: + 'Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged.', + action: parsers.arrayParser, + }, + logLevel: { + env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_LOG_LEVEL', + help: + "The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`.", + default: 'info', + }, + name: { + env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_NAME', + help: + 'The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events.', + required: true, + }, +}; module.exports.DatabaseOptions = { appName: { env: 'PARSE_SERVER_DATABASE_APP_NAME', @@ -1219,6 +1272,12 @@ module.exports.DatabaseOptions = { 'The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances.', action: parsers.numberParser('localThresholdMS'), }, + logClientEvents: { + env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS', + help: 'An array of MongoDB client event configurations to enable logging of specific events.', + action: parsers.arrayParser, + type: 'LogClientEvent[]', + }, maxConnecting: { env: 'PARSE_SERVER_DATABASE_MAX_CONNECTING', help: @@ -1403,30 +1462,32 @@ module.exports.AuthAdapter = { module.exports.LogLevels = { cloudFunctionError: { env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_ERROR', - help: 'Log level used by the Cloud Code Functions on error. Default is `error`.', + help: + 'Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.', default: 'error', }, cloudFunctionSuccess: { env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_SUCCESS', - help: 'Log level used by the Cloud Code Functions on success. Default is `info`.', + help: + 'Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.', default: 'info', }, triggerAfter: { env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER', help: - 'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`.', + 'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values.', default: 'info', }, triggerBeforeError: { env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR', help: - 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`.', + 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.', default: 'error', }, triggerBeforeSuccess: { env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS', help: - 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`.', + 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.', default: 'info', }, }; diff --git a/src/Options/docs.js b/src/Options/docs.js index ade153cdb3..d3e1f258d9 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -238,6 +238,23 @@ * @property {String[]} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.

It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.

Defaults to `^(?![xXsS]?[hH][tT][mM][lL]?$)` which allows any file extension except those MIME types that are mapped to `text/html` and are rendered as website by a web browser. */ +/** + * @interface LogLevel + * @property {StringLiteral} debug Debug level + * @property {StringLiteral} error Error level - highest priority + * @property {StringLiteral} info Info level - default + * @property {StringLiteral} silly Silly level - lowest priority + * @property {StringLiteral} verbose Verbose level + * @property {StringLiteral} warn Warning level + */ + +/** + * @interface LogClientEvent + * @property {String[]} keys Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. + * @property {String} logLevel The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`. + * @property {String} name The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. + */ + /** * @interface DatabaseOptions * @property {String} appName The MongoDB driver option to specify the name of the application that created this MongoClient instance. @@ -262,6 +279,7 @@ * @property {Number} heartbeatFrequencyMS The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment. * @property {Boolean} loadBalanced The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service. * @property {Number} localThresholdMS The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances. + * @property {LogClientEvent[]} logClientEvents An array of MongoDB client event configurations to enable logging of specific events. * @property {Number} maxConnecting The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool. * @property {Number} maxIdleTimeMS The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed. * @property {Number} maxPoolSize The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver. @@ -303,9 +321,9 @@ /** * @interface LogLevels - * @property {String} cloudFunctionError Log level used by the Cloud Code Functions on error. Default is `error`. - * @property {String} cloudFunctionSuccess Log level used by the Cloud Code Functions on success. Default is `info`. - * @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. - * @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. - * @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. + * @property {String} cloudFunctionError Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. + * @property {String} cloudFunctionSuccess Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. + * @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values. + * @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. + * @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. */ diff --git a/src/Options/index.js b/src/Options/index.js index 35b5821987..090dba62e7 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -608,6 +608,32 @@ export interface FileUploadOptions { enableForPublic: ?boolean; } +/* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'silly'` - Silly level (lowest priority) */ +export interface LogLevel { + /* Error level - highest priority */ + error: 'error'; + /* Warning level */ + warn: 'warn'; + /* Info level - default */ + info: 'info'; + /* Verbose level */ + verbose: 'verbose'; + /* Debug level */ + debug: 'debug'; + /* Silly level - lowest priority */ + silly: 'silly'; +} + +export interface LogClientEvent { + /* The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. */ + name: string; + /* Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. */ + keys: ?(string[]); + /* The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`. + :DEFAULT: info */ + logLevel: ?string; +} + export interface DatabaseOptions { /* Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required. :DEFAULT: false */ @@ -725,6 +751,8 @@ export interface DatabaseOptions { createIndexRoleName: ?boolean; /* Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. */ disableIndexFieldValidation: ?boolean; + /* An array of MongoDB client event configurations to enable logging of specific events. */ + logClientEvents: ?(LogClientEvent[]); } export interface AuthAdapter { @@ -736,23 +764,23 @@ export interface AuthAdapter { } export interface LogLevels { - /* Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. + /* Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values. :DEFAULT: info */ triggerAfter: ?string; - /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. + /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. :DEFAULT: info */ triggerBeforeSuccess: ?string; - /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. + /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. :DEFAULT: error */ triggerBeforeError: ?string; - /* Log level used by the Cloud Code Functions on success. Default is `info`. + /* Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. :DEFAULT: info */ cloudFunctionSuccess: ?string; - /* Log level used by the Cloud Code Functions on error. Default is `error`. + /* Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. :DEFAULT: error */ cloudFunctionError: ?string; diff --git a/src/Utils.js b/src/Utils.js index 72b49aeeb2..f46e09de1e 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -410,6 +410,40 @@ class Utils { '%' + char.charCodeAt(0).toString(16).toUpperCase() ); } + + /** + * Creates a JSON replacer function that handles Map, Set, and circular references. + * This replacer can be used with JSON.stringify to safely serialize complex objects. + * + * @returns {Function} A replacer function for JSON.stringify that: + * - Converts Map instances to plain objects + * - Converts Set instances to arrays + * - Replaces circular references with '[Circular]' marker + * + * @example + * const obj = { name: 'test', map: new Map([['key', 'value']]) }; + * obj.self = obj; // circular reference + * JSON.stringify(obj, Utils.getCircularReplacer()); + * // Output: {"name":"test","map":{"key":"value"},"self":"[Circular]"} + */ + static getCircularReplacer() { + const seen = new WeakSet(); + return (key, value) => { + if (value instanceof Map) { + return Object.fromEntries(value); + } + if (value instanceof Set) { + return Array.from(value); + } + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[Circular]'; + } + seen.add(value); + } + return value; + }; + } } module.exports = Utils; From 15c8b1abedc9be191cafcf0b84a9db5406e9655a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 8 Nov 2025 14:49:18 +0000 Subject: [PATCH 08/50] chore(release): 8.5.0-alpha.4 [skip ci] # [8.5.0-alpha.4](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.3...8.5.0-alpha.4) (2025-11-08) ### Features * Add MongoDB client event logging via database option `logClientEvents` ([#9914](https://github.com/parse-community/parse-server/issues/9914)) ([b760733](https://github.com/parse-community/parse-server/commit/b760733b98bcfc9c09ac9780066602e1fda108fe)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 564e64d2e6..1d2c93d306 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.4](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.3...8.5.0-alpha.4) (2025-11-08) + + +### Features + +* Add MongoDB client event logging via database option `logClientEvents` ([#9914](https://github.com/parse-community/parse-server/issues/9914)) ([b760733](https://github.com/parse-community/parse-server/commit/b760733b98bcfc9c09ac9780066602e1fda108fe)) + # [8.5.0-alpha.3](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.2...8.5.0-alpha.3) (2025-11-07) diff --git a/package-lock.json b/package-lock.json index 4c9268211e..eda48969ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.3", + "version": "8.5.0-alpha.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.3", + "version": "8.5.0-alpha.4", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 7c248f9505..4ae2258eea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.3", + "version": "8.5.0-alpha.4", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 4456b02280c2d8dd58b7250e9e67f1a8647b3452 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Sat, 8 Nov 2025 17:02:13 +0100 Subject: [PATCH 09/50] feat: Add Parse Server option `allowPublicExplain` to allow `Parse.Query.explain` without master key (#9890) --- DEPRECATIONS.md | 27 ++--- spec/ParseQuery.spec.js | 99 +++++++++++++++++++ spec/SecurityCheckGroups.spec.js | 20 ++++ .../Storage/Mongo/MongoStorageAdapter.js | 4 +- src/Config.js | 5 + src/Deprecator/Deprecations.js | 1 + src/Options/Definitions.js | 7 ++ src/Options/docs.js | 1 + src/Options/index.js | 3 + .../CheckGroups/CheckGroupServerConfig.js | 15 +++ src/rest.js | 11 +++ types/Options/index.d.ts | 1 + 12 files changed, 180 insertions(+), 14 deletions(-) diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index eb7f463638..6ac20b4616 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -2,19 +2,20 @@ The following is a list of deprecations, according to the [Deprecation Policy](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md#deprecation-policy). After a feature becomes deprecated, and giving developers time to adapt to the change, the deprecated feature will eventually be removed, leading to a breaking change. Developer feedback during the deprecation period may postpone or even revoke the introduction of the breaking change. -| ID | Change | Issue | Deprecation [ℹ️][i_deprecation] | Planned Removal [ℹ️][i_removal] | Status [ℹ️][i_status] | Notes | -|--------|-------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------| -| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - | -| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - | -| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - | -| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - | -| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - | -| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - | -| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - | -| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - | -| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | removed | - | -| DEPPS10 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | deprecated | - | -| DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | deprecated | - | +| ID | Change | Issue | Deprecation [ℹ️][i_deprecation] | Planned Removal [ℹ️][i_removal] | Status [ℹ️][i_status] | Notes | +|---------|----------------------------------------------------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------| +| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - | +| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - | +| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - | +| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - | +| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - | +| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - | +| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - | +| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - | +| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | removed | - | +| DEPPS10 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | deprecated | - | +| DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | deprecated | - | +| DEPPS12 | Database option `allowPublicExplain` will default to `true` | [#7519](https://github.com/parse-community/parse-server/issues/7519) | 8.5.0 (2025) | 9.0.0 (2026) | deprecated | - | [i_deprecation]: ## "The version and date of the deprecation." [i_removal]: ## "The version and date of the planned removal." diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 98ef70564f..2663e649ca 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -8,6 +8,7 @@ const Parse = require('parse/node'); const request = require('../lib/request'); const ParseServerRESTController = require('../lib/ParseServerRESTController').ParseServerRESTController; const ParseServer = require('../lib/ParseServer').default; +const Deprecator = require('../lib/Deprecator/Deprecator').default; const masterKeyHeaders = { 'X-Parse-Application-Id': 'test', @@ -5384,4 +5385,102 @@ describe('Parse.Query testing', () => { expect(query1.length).toEqual(1); }); }); + + describe('allowPublicExplain', () => { + it_id('a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d')(it_only_db('mongo'))( + 'explain works with and without master key when allowPublicExplain is true', + async () => { + await reconfigureServer({ + databaseAdapter: undefined, + databaseURI: 'mongodb://localhost:27017/parse', + databaseOptions: { + allowPublicExplain: true, + }, + }); + + const obj = new TestObject({ foo: 'bar' }); + await obj.save(); + + // Without master key + const query = new Parse.Query(TestObject); + query.explain(); + const resultWithoutMasterKey = await query.find(); + expect(resultWithoutMasterKey).toBeDefined(); + + // With master key + const queryWithMasterKey = new Parse.Query(TestObject); + queryWithMasterKey.explain(); + const resultWithMasterKey = await queryWithMasterKey.find({ useMasterKey: true }); + expect(resultWithMasterKey).toBeDefined(); + } + ); + + it_id('b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e')(it_only_db('mongo'))( + 'explain requires master key when allowPublicExplain is false', + async () => { + await reconfigureServer({ + databaseAdapter: undefined, + databaseURI: 'mongodb://localhost:27017/parse', + databaseOptions: { + allowPublicExplain: false, + }, + }); + + const obj = new TestObject({ foo: 'bar' }); + await obj.save(); + + // Without master key + const query = new Parse.Query(TestObject); + query.explain(); + await expectAsync(query.find()).toBeRejectedWith( + new Parse.Error( + Parse.Error.INVALID_QUERY, + 'Using the explain query parameter requires the master key' + ) + ); + + // With master key + const queryWithMasterKey = new Parse.Query(TestObject); + queryWithMasterKey.explain(); + const result = await queryWithMasterKey.find({ useMasterKey: true }); + expect(result).toBeDefined(); + } + ); + + it_id('c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f')(it_only_db('mongo'))( + 'explain works with and without master key by default', + async () => { + const logger = require('../lib/logger').logger; + const logSpy = spyOn(logger, 'warn').and.callFake(() => {}); + + await reconfigureServer({ + databaseAdapter: undefined, + databaseURI: 'mongodb://localhost:27017/parse', + databaseOptions: { + allowPublicExplain: undefined, + }, + }); + + // Verify deprecation warning is logged when allowPublicExplain is not explicitly set + expect(logSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/DeprecationWarning.*databaseOptions\.allowPublicExplain.*false/) + ); + + const obj = new TestObject({ foo: 'bar' }); + await obj.save(); + + // Without master key + const query = new Parse.Query(TestObject); + query.explain(); + const resultWithoutMasterKey = await query.find(); + expect(resultWithoutMasterKey).toBeDefined(); + + // With master key + const queryWithMasterKey = new Parse.Query(TestObject); + queryWithMasterKey.explain(); + const resultWithMasterKey = await queryWithMasterKey.find({ useMasterKey: true }); + expect(resultWithMasterKey).toBeDefined(); + } + ); + }); }); diff --git a/spec/SecurityCheckGroups.spec.js b/spec/SecurityCheckGroups.spec.js index 3e5f312dd7..aea4468da8 100644 --- a/spec/SecurityCheckGroups.spec.js +++ b/spec/SecurityCheckGroups.spec.js @@ -60,6 +60,26 @@ describe('Security Check Groups', () => { expect(group.checks()[4].checkState()).toBe(CheckState.fail); expect(group.checks()[5].checkState()).toBe(CheckState.fail); }); + + it_only_db('mongo')('checks succeed correctly (MongoDB specific)', async () => { + config.databaseAdapter = undefined; + config.databaseOptions = { allowPublicExplain: false }; + await reconfigureServer(config); + + const group = new CheckGroupServerConfig(); + await group.run(); + expect(group.checks()[6].checkState()).toBe(CheckState.success); + }); + + it_only_db('mongo')('checks fail correctly (MongoDB specific)', async () => { + config.databaseAdapter = undefined; + config.databaseOptions = { allowPublicExplain: true }; + await reconfigureServer(config); + + const group = new CheckGroupServerConfig(); + await group.run(); + expect(group.checks()[6].checkState()).toBe(CheckState.fail); + }); }); describe('CheckGroupDatabase', () => { diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 57f7543085..afe9f39282 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -148,7 +148,7 @@ export class MongoStorageAdapter implements StorageAdapter { this._uri = uri; this._collectionPrefix = collectionPrefix; this._mongoOptions = { ...mongoOptions }; - this._onchange = () => { }; + this._onchange = () => {}; // MaxTimeMS is not a global MongoDB client option, it is applied per operation. this._maxTimeMS = mongoOptions.maxTimeMS; @@ -157,10 +157,12 @@ export class MongoStorageAdapter implements StorageAdapter { this.schemaCacheTtl = mongoOptions.schemaCacheTtl; this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation; this._logClientEvents = mongoOptions.logClientEvents; + // Remove Parse Server-specific options that should not be passed to MongoDB client // Note: We only delete from this._mongoOptions, not from the original mongoOptions object, // because other components (like DatabaseController) need access to these options for (const key of [ + 'allowPublicExplain', 'enableSchemaHooks', 'schemaCacheTtl', 'maxTimeMS', diff --git a/src/Config.js b/src/Config.js index 42b24f2d89..241edf9771 100644 --- a/src/Config.js +++ b/src/Config.js @@ -659,6 +659,11 @@ export class Config { } else if (typeof databaseOptions.schemaCacheTtl !== 'number') { throw `databaseOptions.schemaCacheTtl must be a number`; } + if (databaseOptions.allowPublicExplain === undefined) { + databaseOptions.allowPublicExplain = DatabaseOptions.allowPublicExplain.default; + } else if (typeof databaseOptions.allowPublicExplain !== 'boolean') { + throw `Parse Server option 'databaseOptions.allowPublicExplain' must be a boolean.`; + } } static validateRateLimit(rateLimit) { diff --git a/src/Deprecator/Deprecations.js b/src/Deprecator/Deprecations.js index 970364432b..c63225f5b5 100644 --- a/src/Deprecator/Deprecations.js +++ b/src/Deprecator/Deprecations.js @@ -18,4 +18,5 @@ module.exports = [ { optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' }, { optionKey: 'enableInsecureAuthAdapters', changeNewDefault: 'false' }, + { optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' }, ]; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 6930020de7..1ae9512823 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1137,6 +1137,13 @@ module.exports.LogClientEvent = { }, }; module.exports.DatabaseOptions = { + allowPublicExplain: { + env: 'PARSE_SERVER_DATABASE_ALLOW_PUBLIC_EXPLAIN', + help: + 'Set to `true` to allow `Parse.Query.explain` without master key.

\u26A0\uFE0F Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes.', + action: parsers.booleanParser, + default: true, + }, appName: { env: 'PARSE_SERVER_DATABASE_APP_NAME', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index d3e1f258d9..cdbd06de45 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -257,6 +257,7 @@ /** * @interface DatabaseOptions + * @property {Boolean} allowPublicExplain Set to `true` to allow `Parse.Query.explain` without master key.

⚠️ Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes. * @property {String} appName The MongoDB driver option to specify the name of the application that created this MongoClient instance. * @property {String} authMechanism The MongoDB driver option to specify the authentication mechanism that MongoDB will use to authenticate the connection. * @property {Any} authMechanismProperties The MongoDB driver option to specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs. diff --git a/src/Options/index.js b/src/Options/index.js index 090dba62e7..81dbc3c536 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -751,6 +751,9 @@ export interface DatabaseOptions { createIndexRoleName: ?boolean; /* Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. */ disableIndexFieldValidation: ?boolean; + /* Set to `true` to allow `Parse.Query.explain` without master key.

⚠️ Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes. + :DEFAULT: true */ + allowPublicExplain: ?boolean; /* An array of MongoDB client event configurations to enable logging of specific events. */ logClientEvents: ?(LogClientEvent[]); } diff --git a/src/Security/CheckGroups/CheckGroupServerConfig.js b/src/Security/CheckGroups/CheckGroupServerConfig.js index 05a52a0275..ab2dfc4507 100644 --- a/src/Security/CheckGroups/CheckGroupServerConfig.js +++ b/src/Security/CheckGroups/CheckGroupServerConfig.js @@ -90,6 +90,21 @@ class CheckGroupServerConfig extends CheckGroup { } }, }), + new Check({ + title: 'Public database explain disabled', + warning: + 'Database explain queries are publicly accessible, which may expose sensitive database performance information and schema details.', + solution: + "Change Parse Server configuration to 'databaseOptions.allowPublicExplain: false'. You will need to use master key to run explain queries.", + check: () => { + if ( + config.databaseOptions?.allowPublicExplain === true || + config.databaseOptions?.allowPublicExplain == null + ) { + throw 1; + } + }, + }), ]; } } diff --git a/src/rest.js b/src/rest.js index 8297121a68..e2e688a972 100644 --- a/src/rest.js +++ b/src/rest.js @@ -35,6 +35,17 @@ async function runFindTriggers( ) { const { isGet } = options; + if (restOptions && restOptions.explain && !auth.isMaster) { + const allowPublicExplain = config.databaseOptions?.allowPublicExplain ?? true; + + if (!allowPublicExplain) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'Using the explain query parameter requires the master key' + ); + } + } + // Run beforeFind trigger - may modify query or return objects directly const result = await triggers.maybeRunQueryTrigger( triggers.Types.beforeFind, diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index 3cb604bbe1..3332d0693c 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -238,6 +238,7 @@ export interface DatabaseOptions { authSource?: string; autoSelectFamily?: boolean; autoSelectFamilyAttemptTimeout?: number; + allowPublicExplain?: boolean; compressors?: string[] | string; connectTimeoutMS?: number; directConnection?: boolean; From 502a512028a369e0454ff18f31e76b8287f71944 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 8 Nov 2025 16:03:05 +0000 Subject: [PATCH 10/50] chore(release): 8.5.0-alpha.5 [skip ci] # [8.5.0-alpha.5](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.4...8.5.0-alpha.5) (2025-11-08) ### Features * Add Parse Server option `allowPublicExplain` to allow `Parse.Query.explain` without master key ([#9890](https://github.com/parse-community/parse-server/issues/9890)) ([4456b02](https://github.com/parse-community/parse-server/commit/4456b02280c2d8dd58b7250e9e67f1a8647b3452)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 1d2c93d306..e7e661b7da 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.5](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.4...8.5.0-alpha.5) (2025-11-08) + + +### Features + +* Add Parse Server option `allowPublicExplain` to allow `Parse.Query.explain` without master key ([#9890](https://github.com/parse-community/parse-server/issues/9890)) ([4456b02](https://github.com/parse-community/parse-server/commit/4456b02280c2d8dd58b7250e9e67f1a8647b3452)) + # [8.5.0-alpha.4](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.3...8.5.0-alpha.4) (2025-11-08) diff --git a/package-lock.json b/package-lock.json index eda48969ca..3fa28645bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.4", + "version": "8.5.0-alpha.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.4", + "version": "8.5.0-alpha.5", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 4ae2258eea..b58afa61a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.4", + "version": "8.5.0-alpha.5", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From d3d4003570b9872f2b0f5a25fc06ce4c4132860d Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 18:41:45 +0100 Subject: [PATCH 11/50] fix: `GridFSBucketAdapter` throws when using some Parse Server specific options in MongoDB database options (#9915) --- spec/GridFSBucketStorageAdapter.spec.js | 12 ++++++++- src/Adapters/Files/GridFSBucketAdapter.js | 8 +++--- .../Storage/Mongo/MongoStorageAdapter.js | 27 +++++-------------- src/defaults.js | 18 +++++++++++++ types/Options/index.d.ts | 16 ++++++++--- 5 files changed, 53 insertions(+), 28 deletions(-) diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js index d30415edf3..8e1e4f2900 100644 --- a/spec/GridFSBucketStorageAdapter.spec.js +++ b/spec/GridFSBucketStorageAdapter.spec.js @@ -24,10 +24,20 @@ describe_only_db('mongo')('GridFSBucket', () => { const databaseURI = 'mongodb://localhost:27017/parse'; const gfsAdapter = new GridFSBucketAdapter(databaseURI, { retryWrites: true, - // these are not supported by the mongo client + // Parse Server-specific options that should be filtered out before passing to MongoDB client + allowPublicExplain: true, enableSchemaHooks: true, schemaCacheTtl: 5000, maxTimeMS: 30000, + disableIndexFieldValidation: true, + logClientEvents: [{ name: 'commandStarted' }], + createIndexUserUsername: true, + createIndexUserUsernameCaseInsensitive: true, + createIndexUserEmail: true, + createIndexUserEmailCaseInsensitive: true, + createIndexUserEmailVerifyToken: true, + createIndexUserPasswordResetToken: true, + createIndexRoleName: true, }); const db = await gfsAdapter._connect(); diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 45a585ecc2..b301d1c0c1 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -9,7 +9,7 @@ // @flow-disable-next import { MongoClient, GridFSBucket, Db } from 'mongodb'; import { FilesAdapter, validateFilename } from './FilesAdapter'; -import defaults from '../../defaults'; +import defaults, { ParseServerDatabaseOptions } from '../../defaults'; const crypto = require('crypto'); export class GridFSBucketAdapter extends FilesAdapter { @@ -34,10 +34,10 @@ export class GridFSBucketAdapter extends FilesAdapter { .digest('base64') .substring(0, 32) : null; - const defaultMongoOptions = { - }; + const defaultMongoOptions = {}; const _mongoOptions = Object.assign(defaultMongoOptions, mongoOptions); - for (const key of ['enableSchemaHooks', 'schemaCacheTtl', 'maxTimeMS', 'disableIndexFieldValidation']) { + // Remove Parse Server-specific options that should not be passed to MongoDB client + for (const key of ParseServerDatabaseOptions) { delete _mongoOptions[key]; } this._mongoOptions = _mongoOptions; diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index afe9f39282..50fd348861 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -16,7 +16,7 @@ import { import Parse from 'parse/node'; // @flow-disable-next import _ from 'lodash'; -import defaults from '../../../defaults'; +import defaults, { ParseServerDatabaseOptions } from '../../../defaults'; import logger from '../../../logger'; import Utils from '../../../Utils'; @@ -147,7 +147,6 @@ export class MongoStorageAdapter implements StorageAdapter { constructor({ uri = defaults.DefaultMongoURI, collectionPrefix = '', mongoOptions = {} }: any) { this._uri = uri; this._collectionPrefix = collectionPrefix; - this._mongoOptions = { ...mongoOptions }; this._onchange = () => {}; // MaxTimeMS is not a global MongoDB client option, it is applied per operation. @@ -158,24 +157,12 @@ export class MongoStorageAdapter implements StorageAdapter { this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation; this._logClientEvents = mongoOptions.logClientEvents; - // Remove Parse Server-specific options that should not be passed to MongoDB client - // Note: We only delete from this._mongoOptions, not from the original mongoOptions object, - // because other components (like DatabaseController) need access to these options - for (const key of [ - 'allowPublicExplain', - 'enableSchemaHooks', - 'schemaCacheTtl', - 'maxTimeMS', - 'disableIndexFieldValidation', - 'logClientEvents', - 'createIndexUserUsername', - 'createIndexUserUsernameCaseInsensitive', - 'createIndexUserEmail', - 'createIndexUserEmailCaseInsensitive', - 'createIndexUserEmailVerifyToken', - 'createIndexUserPasswordResetToken', - 'createIndexRoleName', - ]) { + // Create a copy of mongoOptions and remove Parse Server-specific options that should not + // be passed to MongoDB client. Note: We only delete from this._mongoOptions, not from the + // original mongoOptions object, because other components (like DatabaseController) need + // access to these options. + this._mongoOptions = { ...mongoOptions }; + for (const key of ParseServerDatabaseOptions) { delete this._mongoOptions[key]; } } diff --git a/src/defaults.js b/src/defaults.js index a2b105d8db..07eeb51360 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -33,3 +33,21 @@ const computedDefaults = { export default Object.assign({}, DefinitionDefaults, computedDefaults); export const DefaultMongoURI = DefinitionDefaults.databaseURI; + +// Parse Server-specific database options that should be filtered out +// before passing to MongoDB client +export const ParseServerDatabaseOptions = [ + 'allowPublicExplain', + 'createIndexRoleName', + 'createIndexUserEmail', + 'createIndexUserEmailCaseInsensitive', + 'createIndexUserEmailVerifyToken', + 'createIndexUserPasswordResetToken', + 'createIndexUserUsername', + 'createIndexUserUsernameCaseInsensitive', + 'disableIndexFieldValidation', + 'enableSchemaHooks', + 'logClientEvents', + 'maxTimeMS', + 'schemaCacheTtl', +]; diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index 3332d0693c..ad11050648 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -228,9 +228,21 @@ export interface FileUploadOptions { } export interface DatabaseOptions { // Parse Server custom options + allowPublicExplain?: boolean; + createIndexRoleName?: boolean; + createIndexUserEmail?: boolean; + createIndexUserEmailCaseInsensitive?: boolean; + createIndexUserEmailVerifyToken?: boolean; + createIndexUserPasswordResetToken?: boolean; + createIndexUserUsername?: boolean; + createIndexUserUsernameCaseInsensitive?: boolean; + disableIndexFieldValidation?: boolean; enableSchemaHooks?: boolean; + logClientEvents?: any[]; + // maxTimeMS is a MongoDB option but Parse Server applies it per-operation, not as a global client option + maxTimeMS?: number; schemaCacheTtl?: number; - + // MongoDB driver options appName?: string; authMechanism?: string; @@ -238,7 +250,6 @@ export interface DatabaseOptions { authSource?: string; autoSelectFamily?: boolean; autoSelectFamilyAttemptTimeout?: number; - allowPublicExplain?: boolean; compressors?: string[] | string; connectTimeoutMS?: number; directConnection?: boolean; @@ -250,7 +261,6 @@ export interface DatabaseOptions { maxIdleTimeMS?: number; maxPoolSize?: number; maxStalenessSeconds?: number; - maxTimeMS?: number; minPoolSize?: number; proxyHost?: string; proxyPassword?: string; From 9f1fc7cb8ae8976ad8a58515f81ebadf50b93d3d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 8 Nov 2025 17:42:37 +0000 Subject: [PATCH 12/50] chore(release): 8.5.0-alpha.6 [skip ci] # [8.5.0-alpha.6](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.5...8.5.0-alpha.6) (2025-11-08) ### Bug Fixes * `GridFSBucketAdapter` throws when using some Parse Server specific options in MongoDB database options ([#9915](https://github.com/parse-community/parse-server/issues/9915)) ([d3d4003](https://github.com/parse-community/parse-server/commit/d3d4003570b9872f2b0f5a25fc06ce4c4132860d)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index e7e661b7da..87da90beb5 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.6](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.5...8.5.0-alpha.6) (2025-11-08) + + +### Bug Fixes + +* `GridFSBucketAdapter` throws when using some Parse Server specific options in MongoDB database options ([#9915](https://github.com/parse-community/parse-server/issues/9915)) ([d3d4003](https://github.com/parse-community/parse-server/commit/d3d4003570b9872f2b0f5a25fc06ce4c4132860d)) + # [8.5.0-alpha.5](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.4...8.5.0-alpha.5) (2025-11-08) diff --git a/package-lock.json b/package-lock.json index 3fa28645bc..27afc8aec0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.5", + "version": "8.5.0-alpha.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.5", + "version": "8.5.0-alpha.6", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index b58afa61a5..9ace6e7e59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.5", + "version": "8.5.0-alpha.6", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 3c9af48edd999158443b797e388e29495953799e Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 20:04:31 +0100 Subject: [PATCH 13/50] perf: Upgrade MongoDB driver to 6.20.0 (#9887) --- package-lock.json | 41 ++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 27afc8aec0..c66eebdfe7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "lodash": "4.17.21", "lru-cache": "10.4.0", "mime": "4.0.7", - "mongodb": "6.17.0", + "mongodb": "6.20.0", "mustache": "4.2.0", "otpauth": "9.4.0", "parse": "7.0.1", @@ -14950,14 +14950,13 @@ } }, "node_modules/mongodb": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz", - "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==", - "license": "Apache-2.0", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", + "@mongodb-js/saslprep": "^1.3.0", "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.0" + "mongodb-connection-string-url": "^3.0.2" }, "engines": { "node": ">=16.20.1" @@ -14968,7 +14967,7 @@ "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", + "snappy": "^7.3.2", "socks": "^2.7.1" }, "peerDependenciesMeta": { @@ -14996,12 +14995,12 @@ } }, "node_modules/mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", "dependencies": { "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" + "whatwg-url": "^14.1.0 || ^13.0.0" } }, "node_modules/mongodb-download-url": { @@ -33390,22 +33389,22 @@ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" }, "mongodb": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz", - "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", "requires": { - "@mongodb-js/saslprep": "^1.1.9", + "@mongodb-js/saslprep": "^1.3.0", "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.0" + "mongodb-connection-string-url": "^3.0.2" } }, "mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", "requires": { "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" + "whatwg-url": "^14.1.0 || ^13.0.0" } }, "mongodb-download-url": { diff --git a/package.json b/package.json index 9ace6e7e59..ba941f4001 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "lodash": "4.17.21", "lru-cache": "10.4.0", "mime": "4.0.7", - "mongodb": "6.17.0", + "mongodb": "6.20.0", "mustache": "4.2.0", "otpauth": "9.4.0", "parse": "7.0.1", From 76826447f88336704eed758baea662b5b3cc701e Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 8 Nov 2025 19:05:24 +0000 Subject: [PATCH 14/50] chore(release): 8.5.0-alpha.7 [skip ci] # [8.5.0-alpha.7](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.6...8.5.0-alpha.7) (2025-11-08) ### Performance Improvements * Upgrade MongoDB driver to 6.20.0 ([#9887](https://github.com/parse-community/parse-server/issues/9887)) ([3c9af48](https://github.com/parse-community/parse-server/commit/3c9af48edd999158443b797e388e29495953799e)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 87da90beb5..045b3ae5ab 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.7](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.6...8.5.0-alpha.7) (2025-11-08) + + +### Performance Improvements + +* Upgrade MongoDB driver to 6.20.0 ([#9887](https://github.com/parse-community/parse-server/issues/9887)) ([3c9af48](https://github.com/parse-community/parse-server/commit/3c9af48edd999158443b797e388e29495953799e)) + # [8.5.0-alpha.6](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.5...8.5.0-alpha.6) (2025-11-08) diff --git a/package-lock.json b/package-lock.json index c66eebdfe7..70067284a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.6", + "version": "8.5.0-alpha.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.6", + "version": "8.5.0-alpha.7", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index ba941f4001..add0d15339 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.6", + "version": "8.5.0-alpha.7", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 92788a19d337a900fad8062ebb5415c233b88cbc Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:21:43 +0100 Subject: [PATCH 15/50] ci: Add performance impact step to CI (#9916) --- .github/workflows/ci-performance.yml | 291 ++++++++++++++++++++++ CONTRIBUTING.md | 58 ++++- benchmark/performance.js | 354 +++++++++++++++++++++++++++ package.json | 5 +- 4 files changed, 706 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci-performance.yml create mode 100644 benchmark/performance.js diff --git a/.github/workflows/ci-performance.yml b/.github/workflows/ci-performance.yml new file mode 100644 index 0000000000..4cde4d97b0 --- /dev/null +++ b/.github/workflows/ci-performance.yml @@ -0,0 +1,291 @@ +name: ci-performance +on: + pull_request: + branches: + - alpha + - beta + - release + - 'release-[0-9]+.x.x' + - next-major + paths-ignore: + - '**.md' + - 'docs/**' + +env: + NODE_VERSION: 24.11.0 + MONGODB_VERSION: 8.0.4 + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + performance-check: + name: Benchmarks + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout base branch + uses: actions/checkout@v4 + with: + ref: ${{ github.base_ref }} + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies (base) + run: npm ci + + - name: Build Parse Server (base) + run: npm run build + + - name: Run baseline benchmarks + id: baseline + run: | + echo "Checking if benchmark script exists..." + if [ ! -f "benchmark/performance.js" ]; then + echo "⚠️ Benchmark script not found in base branch - this is expected for new features" + echo "Skipping baseline benchmark" + echo '[]' > baseline.json + echo "Baseline: N/A (benchmark script not in base branch)" > baseline-output.txt + exit 0 + fi + echo "Running baseline benchmarks..." + npm run benchmark > baseline-output.txt 2>&1 || true + echo "Benchmark command completed with exit code: $?" + echo "Output file size: $(wc -c < baseline-output.txt) bytes" + echo "--- Begin baseline-output.txt ---" + cat baseline-output.txt + echo "--- End baseline-output.txt ---" + # Extract JSON from output (everything between first [ and last ]) + sed -n '/^\[/,/^\]/p' baseline-output.txt > baseline.json || echo '[]' > baseline.json + echo "Extracted JSON size: $(wc -c < baseline.json) bytes" + echo "Baseline benchmark results:" + cat baseline.json + continue-on-error: true + + - name: Upload baseline results + uses: actions/upload-artifact@v4 + with: + name: baseline-benchmark + path: | + baseline.json + baseline-output.txt + retention-days: 7 + + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 1 + clean: true + + - name: Setup Node.js (PR) + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies (PR) + run: npm ci + + - name: Build Parse Server (PR) + run: npm run build + + - name: Run PR benchmarks + id: pr-bench + run: | + echo "Running PR benchmarks..." + npm run benchmark > pr-output.txt 2>&1 || true + echo "Benchmark command completed with exit code: $?" + echo "Output file size: $(wc -c < pr-output.txt) bytes" + echo "--- Begin pr-output.txt ---" + cat pr-output.txt + echo "--- End pr-output.txt ---" + # Extract JSON from output (everything between first [ and last ]) + sed -n '/^\[/,/^\]/p' pr-output.txt > pr.json || echo '[]' > pr.json + echo "Extracted JSON size: $(wc -c < pr.json) bytes" + echo "PR benchmark results:" + cat pr.json + continue-on-error: true + + - name: Upload PR results + uses: actions/upload-artifact@v4 + with: + name: pr-benchmark + path: | + pr.json + pr-output.txt + retention-days: 7 + + - name: Verify benchmark files exist + run: | + echo "Checking for benchmark result files..." + if [ ! -f baseline.json ] || [ ! -s baseline.json ]; then + echo "⚠️ baseline.json is missing or empty, creating empty array" + echo '[]' > baseline.json + fi + if [ ! -f pr.json ] || [ ! -s pr.json ]; then + echo "⚠️ pr.json is missing or empty, creating empty array" + echo '[]' > pr.json + fi + echo "baseline.json size: $(wc -c < baseline.json) bytes" + echo "pr.json size: $(wc -c < pr.json) bytes" + + - name: Store benchmark result (PR) + uses: benchmark-action/github-action-benchmark@v1 + if: github.event_name == 'pull_request' && hashFiles('pr.json') != '' + continue-on-error: true + with: + name: Parse Server Performance + tool: 'customSmallerIsBetter' + output-file-path: pr.json + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: false + save-data-file: false + alert-threshold: '110%' + comment-on-alert: true + fail-on-alert: false + alert-comment-cc-users: '@parse-community/maintainers' + summary-always: true + + - name: Compare benchmark results + id: compare + run: | + node -e " + const fs = require('fs'); + + let baseline, pr; + try { + baseline = JSON.parse(fs.readFileSync('baseline.json', 'utf8')); + pr = JSON.parse(fs.readFileSync('pr.json', 'utf8')); + } catch (e) { + console.log('⚠️ Could not parse benchmark results'); + process.exit(0); + } + + // Handle case where baseline doesn't exist (new feature) + if (!Array.isArray(baseline) || baseline.length === 0) { + if (!Array.isArray(pr) || pr.length === 0) { + console.log('⚠️ Benchmark results are empty or invalid'); + process.exit(0); + } + console.log('# Performance Benchmark Results\n'); + console.log('> ℹ️ Baseline not available - this appears to be a new feature\n'); + console.log('| Benchmark | Value | Details |'); + console.log('|-----------|-------|---------|'); + pr.forEach(result => { + console.log(\`| \${result.name} | \${result.value.toFixed(2)} ms | \${result.extra} |\`); + }); + console.log(''); + console.log('✅ **New benchmarks established for this feature.**'); + process.exit(0); + } + + if (!Array.isArray(pr) || pr.length === 0) { + console.log('⚠️ PR benchmark results are empty or invalid'); + process.exit(0); + } + + console.log('# Performance Comparison\n'); + console.log('| Benchmark | Baseline | PR | Change | Status |'); + console.log('|-----------|----------|----|---------| ------ |'); + + let hasRegression = false; + let hasImprovement = false; + + baseline.forEach(baseResult => { + const prResult = pr.find(p => p.name === baseResult.name); + if (!prResult) { + console.log(\`| \${baseResult.name} | \${baseResult.value.toFixed(2)} ms | N/A | - | ⚠️ Missing |\`); + return; + } + + const baseValue = parseFloat(baseResult.value); + const prValue = parseFloat(prResult.value); + const change = ((prValue - baseValue) / baseValue * 100); + const changeStr = change > 0 ? \`+\${change.toFixed(1)}%\` : \`\${change.toFixed(1)}%\`; + + let status = '✅'; + if (change > 20) { + status = '❌ Much Slower'; + hasRegression = true; + } else if (change > 10) { + status = '⚠️ Slower'; + hasRegression = true; + } else if (change < -10) { + status = '🚀 Faster'; + hasImprovement = true; + } + + console.log(\`| \${baseResult.name} | \${baseValue.toFixed(2)} ms | \${prValue.toFixed(2)} ms | \${changeStr} | \${status} |\`); + }); + + console.log(''); + if (hasRegression) { + console.log('⚠️ **Performance regressions detected.** Please review the changes.'); + } else if (hasImprovement) { + console.log('🚀 **Performance improvements detected!** Great work!'); + } else { + console.log('✅ **No significant performance changes.**'); + } + " | tee comparison.md + + - name: Upload comparison + uses: actions/upload-artifact@v4 + with: + name: benchmark-comparison + path: comparison.md + retention-days: 30 + + - name: Prepare comment body + if: github.event_name == 'pull_request' + run: | + echo "## Performance Impact Report" > comment.md + echo "" >> comment.md + if [ -f comparison.md ]; then + cat comparison.md >> comment.md + else + echo "⚠️ Could not generate performance comparison." >> comment.md + fi + echo "" >> comment.md + echo "
" >> comment.md + echo "📊 View detailed results" >> comment.md + echo "" >> comment.md + echo "### Baseline Results" >> comment.md + echo "\`\`\`json" >> comment.md + cat baseline.json >> comment.md + echo "\`\`\`" >> comment.md + echo "" >> comment.md + echo "### PR Results" >> comment.md + echo "\`\`\`json" >> comment.md + cat pr.json >> comment.md + echo "\`\`\`" >> comment.md + echo "" >> comment.md + echo "
" >> comment.md + echo "" >> comment.md + echo "*Benchmarks ran with ${BENCHMARK_ITERATIONS:-100} iterations per test on Node.js ${{ env.NODE_VERSION }}*" >> comment.md + + - name: Comment PR with results + if: github.event_name == 'pull_request' + uses: thollander/actions-comment-pull-request@v2 + continue-on-error: true + with: + filePath: comment.md + comment_tag: performance-benchmark + mode: recreate + + - name: Generate job summary + if: always() + run: | + if [ -f comparison.md ]; then + cat comparison.md >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Benchmark comparison not available" >> $GITHUB_STEP_SUMMARY + fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 01c88df10c..f79caa4236 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,9 +21,13 @@ - [Good to Know](#good-to-know) - [Troubleshooting](#troubleshooting) - [Please Do's](#please-dos) - - [TypeScript Tests](#typescript-tests) + - [TypeScript Tests](#typescript-tests) - [Test against Postgres](#test-against-postgres) - [Postgres with Docker](#postgres-with-docker) + - [Performance Testing](#performance-testing) + - [Adding Tests](#adding-tests) + - [Adding Benchmarks](#adding-benchmarks) + - [Benchmark Guidelines](#benchmark-guidelines) - [Breaking Changes](#breaking-changes) - [Deprecation Policy](#deprecation-policy) - [Feature Considerations](#feature-considerations) @@ -298,6 +302,58 @@ RUN chmod +x /docker-entrypoint-initdb.d/setup-dbs.sh Note that the script above will ONLY be executed during initialization of the container with no data in the database, see the official [Postgres image](https://hub.docker.com/_/postgres) for details. If you want to use the script to run again be sure there is no data in the /var/lib/postgresql/data of the container. +### Performance Testing + +Parse Server includes an automated performance benchmarking system that runs on every pull request to detect performance regressions and track improvements over time. + +#### Adding Tests + +You should consider adding performance benchmarks if your contribution: + +- **Introduces a performance-critical feature**: Features that will be frequently used in production environments, such as new query operations, authentication methods, or data processing functions. +- **Modifies existing critical paths**: Changes to core functionality like object CRUD operations, query execution, user authentication, file operations, or Cloud Code execution. +- **Has potential performance impact**: Any change that affects database operations, network requests, data parsing, caching mechanisms, or algorithmic complexity. +- **Optimizes performance**: If your PR specifically aims to improve performance, adding benchmarks helps verify the improvement and prevents future regressions. + +#### Adding Benchmarks + +Performance benchmarks are located in [`benchmark/performance.js`](benchmark/performance.js). To add a new benchmark: + +1. **Identify the operation to benchmark**: Determine the specific operation you want to measure (e.g., a new query type, a new API endpoint). + +2. **Create a benchmark function**: Follow the existing patterns in `benchmark/performance.js`: + ```javascript + async function benchmarkNewFeature() { + return measureOperation('Feature Name', async () => { + // Your operation to benchmark + const result = await someOperation(); + }, ITERATIONS); + } + ``` + +3. **Add to benchmark suite**: Register your benchmark in the `runBenchmarks()` function: + ```javascript + console.error('Running New Feature benchmark...'); + await cleanupDatabase(); + results.push(await benchmarkNewFeature()); + ``` + +4. **Test locally**: Run the benchmarks locally to verify they work: + ```bash + npm run benchmark:quick # Quick test with 10 iterations + npm run benchmark # Full test with 100 iterations + ``` + +For new features where no baseline exists, the CI will establish new benchmarks that future PRs will be compared against. + +#### Benchmark Guidelines + +- **Keep benchmarks focused**: Each benchmark should test a single, well-defined operation. +- **Use realistic data**: Test with data that reflects real-world usage patterns. +- **Clean up between runs**: Use `cleanupDatabase()` to ensure consistent test conditions. +- **Consider iteration count**: Use fewer iterations for expensive operations (see `ITERATIONS` environment variable). +- **Document what you're testing**: Add clear comments explaining what the benchmark measures and why it's important. + ## Breaking Changes Breaking changes should be avoided whenever possible. For a breaking change to be accepted, the benefits of the change have to clearly outweigh the costs of developers having to adapt their deployments. If a breaking change is only cosmetic it will likely be rejected and preferred to become obsolete organically during the course of further development, unless it is required as part of a larger change. Breaking changes should follow the [Deprecation Policy](#deprecation-policy). diff --git a/benchmark/performance.js b/benchmark/performance.js new file mode 100644 index 0000000000..831a57db37 --- /dev/null +++ b/benchmark/performance.js @@ -0,0 +1,354 @@ +/** + * Performance Benchmark Suite for Parse Server + * + * This suite measures the performance of critical Parse Server operations + * using the Node.js Performance API. Results are output in a format + * compatible with github-action-benchmark. + * + * Run with: npm run benchmark + */ + +const Parse = require('parse/node'); +const { performance, PerformanceObserver } = require('perf_hooks'); +const { MongoClient } = require('mongodb'); + +// Configuration +const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/parse_benchmark_test'; +const SERVER_URL = 'http://localhost:1337/parse'; +const APP_ID = 'benchmark-app-id'; +const MASTER_KEY = 'benchmark-master-key'; +const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '100', 10); + +// Parse Server instance +let parseServer; +let mongoClient; + +/** + * Initialize Parse Server for benchmarking + */ +async function initializeParseServer() { + const express = require('express'); + const { default: ParseServer } = require('../lib/index.js'); + + const app = express(); + + parseServer = new ParseServer({ + databaseURI: MONGODB_URI, + appId: APP_ID, + masterKey: MASTER_KEY, + serverURL: SERVER_URL, + silent: true, + allowClientClassCreation: true, + }); + + app.use('/parse', parseServer.app); + + return new Promise((resolve, reject) => { + const server = app.listen(1337, (err) => { + if (err) { + reject(new Error(`Failed to start server: ${err.message}`)); + return; + } + Parse.initialize(APP_ID); + Parse.masterKey = MASTER_KEY; + Parse.serverURL = SERVER_URL; + resolve(server); + }); + + server.on('error', (err) => { + reject(new Error(`Server error: ${err.message}`)); + }); + }); +} + +/** + * Clean up database between benchmarks + */ +async function cleanupDatabase() { + try { + if (!mongoClient) { + mongoClient = await MongoClient.connect(MONGODB_URI); + } + const db = mongoClient.db(); + const collections = await db.listCollections().toArray(); + + for (const collection of collections) { + if (!collection.name.startsWith('system.')) { + await db.collection(collection.name).deleteMany({}); + } + } + } catch (error) { + throw new Error(`Failed to cleanup database: ${error.message}`); + } +} + +/** + * Measure average time for an async operation over multiple iterations + */ +async function measureOperation(name, operation, iterations = ITERATIONS) { + const times = []; + + for (let i = 0; i < iterations; i++) { + const start = performance.now(); + await operation(); + const end = performance.now(); + times.push(end - start); + } + + // Calculate statistics + times.sort((a, b) => a - b); + const sum = times.reduce((acc, val) => acc + val, 0); + const mean = sum / times.length; + const p50 = times[Math.floor(times.length * 0.5)]; + const p95 = times[Math.floor(times.length * 0.95)]; + const p99 = times[Math.floor(times.length * 0.99)]; + const min = times[0]; + const max = times[times.length - 1]; + + return { + name, + value: mean, + unit: 'ms', + range: `${min.toFixed(2)} - ${max.toFixed(2)}`, + extra: `p50: ${p50.toFixed(2)}ms, p95: ${p95.toFixed(2)}ms, p99: ${p99.toFixed(2)}ms`, + }; +} + +/** + * Benchmark: Object Create + */ +async function benchmarkObjectCreate() { + let counter = 0; + + return measureOperation('Object Create', async () => { + const TestObject = Parse.Object.extend('BenchmarkTest'); + const obj = new TestObject(); + obj.set('testField', `test-value-${counter++}`); + obj.set('number', counter); + obj.set('boolean', true); + await obj.save(); + }); +} + +/** + * Benchmark: Object Read (by ID) + */ +async function benchmarkObjectRead() { + // Setup: Create test objects + const TestObject = Parse.Object.extend('BenchmarkTest'); + const objects = []; + + for (let i = 0; i < ITERATIONS; i++) { + const obj = new TestObject(); + obj.set('testField', `read-test-${i}`); + objects.push(obj); + } + + await Parse.Object.saveAll(objects); + + let counter = 0; + + return measureOperation('Object Read', async () => { + const query = new Parse.Query('BenchmarkTest'); + await query.get(objects[counter++ % objects.length].id); + }); +} + +/** + * Benchmark: Object Update + */ +async function benchmarkObjectUpdate() { + // Setup: Create test objects + const TestObject = Parse.Object.extend('BenchmarkTest'); + const objects = []; + + for (let i = 0; i < ITERATIONS; i++) { + const obj = new TestObject(); + obj.set('testField', `update-test-${i}`); + obj.set('counter', 0); + objects.push(obj); + } + + await Parse.Object.saveAll(objects); + + let counter = 0; + + return measureOperation('Object Update', async () => { + const obj = objects[counter++ % objects.length]; + obj.increment('counter'); + obj.set('lastUpdated', new Date()); + await obj.save(); + }); +} + +/** + * Benchmark: Simple Query + */ +async function benchmarkSimpleQuery() { + // Setup: Create test data + const TestObject = Parse.Object.extend('BenchmarkTest'); + const objects = []; + + for (let i = 0; i < 100; i++) { + const obj = new TestObject(); + obj.set('category', i % 10); + obj.set('value', i); + objects.push(obj); + } + + await Parse.Object.saveAll(objects); + + let counter = 0; + + return measureOperation('Simple Query', async () => { + const query = new Parse.Query('BenchmarkTest'); + query.equalTo('category', counter++ % 10); + await query.find(); + }); +} + +/** + * Benchmark: Batch Save (saveAll) + */ +async function benchmarkBatchSave() { + const BATCH_SIZE = 10; + + return measureOperation('Batch Save (10 objects)', async () => { + const TestObject = Parse.Object.extend('BenchmarkTest'); + const objects = []; + + for (let i = 0; i < BATCH_SIZE; i++) { + const obj = new TestObject(); + obj.set('batchField', `batch-${i}`); + obj.set('timestamp', new Date()); + objects.push(obj); + } + + await Parse.Object.saveAll(objects); + }, Math.floor(ITERATIONS / BATCH_SIZE)); // Fewer iterations for batch operations +} + +/** + * Benchmark: User Signup + */ +async function benchmarkUserSignup() { + let counter = 0; + + return measureOperation('User Signup', async () => { + counter++; + const user = new Parse.User(); + user.set('username', `benchmark_user_${Date.now()}_${counter}`); + user.set('password', 'benchmark_password'); + user.set('email', `benchmark${counter}@example.com`); + await user.signUp(); + }, Math.floor(ITERATIONS / 10)); // Fewer iterations for user operations +} + +/** + * Benchmark: User Login + */ +async function benchmarkUserLogin() { + // Setup: Create test users + const users = []; + + for (let i = 0; i < 10; i++) { + const user = new Parse.User(); + user.set('username', `benchmark_login_user_${i}`); + user.set('password', 'benchmark_password'); + user.set('email', `login${i}@example.com`); + await user.signUp(); + users.push({ username: user.get('username'), password: 'benchmark_password' }); + await Parse.User.logOut(); + } + + let counter = 0; + + return measureOperation('User Login', async () => { + const userCreds = users[counter++ % users.length]; + await Parse.User.logIn(userCreds.username, userCreds.password); + await Parse.User.logOut(); + }, Math.floor(ITERATIONS / 10)); // Fewer iterations for user operations +} + +/** + * Run all benchmarks + */ +async function runBenchmarks() { + console.error('Starting Parse Server Performance Benchmarks...'); + console.error(`Iterations per benchmark: ${ITERATIONS}`); + console.error(''); + + let server; + + try { + // Initialize Parse Server + console.error('Initializing Parse Server...'); + server = await initializeParseServer(); + + // Wait for server to be ready + await new Promise(resolve => setTimeout(resolve, 2000)); + + const results = []; + + // Run each benchmark with database cleanup + console.error('Running Object Create benchmark...'); + await cleanupDatabase(); + results.push(await benchmarkObjectCreate()); + + console.error('Running Object Read benchmark...'); + await cleanupDatabase(); + results.push(await benchmarkObjectRead()); + + console.error('Running Object Update benchmark...'); + await cleanupDatabase(); + results.push(await benchmarkObjectUpdate()); + + console.error('Running Simple Query benchmark...'); + await cleanupDatabase(); + results.push(await benchmarkSimpleQuery()); + + console.error('Running Batch Save benchmark...'); + await cleanupDatabase(); + results.push(await benchmarkBatchSave()); + + console.error('Running User Signup benchmark...'); + await cleanupDatabase(); + results.push(await benchmarkUserSignup()); + + console.error('Running User Login benchmark...'); + await cleanupDatabase(); + results.push(await benchmarkUserLogin()); + + // Output results in github-action-benchmark format + console.log(JSON.stringify(results, null, 2)); + + console.error(''); + console.error('Benchmarks completed successfully!'); + console.error(''); + console.error('Summary:'); + results.forEach(result => { + console.error(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`); + }); + + } catch (error) { + console.error('Error running benchmarks:', error); + process.exit(1); + } finally { + // Cleanup + if (mongoClient) { + await mongoClient.close(); + } + if (server) { + server.close(); + } + // Give some time for cleanup + setTimeout(() => process.exit(0), 1000); + } +} + +// Run benchmarks if executed directly +if (require.main === module) { + runBenchmarks(); +} + +module.exports = { runBenchmarks }; diff --git a/package.json b/package.json index add0d15339..fe043e8ee4 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,10 @@ "prettier": "prettier --write {src,spec}/{**/*,*}.js", "prepare": "npm run build", "postinstall": "node -p 'require(\"./postinstall.js\")()'", - "madge:circular": "node_modules/.bin/madge ./src --circular" + "madge:circular": "node_modules/.bin/madge ./src --circular", + "benchmark": "cross-env MONGODB_VERSION=8.0.4 MONGODB_TOPOLOGY=standalone mongodb-runner exec -t standalone --version 8.0.4 -- --port 27017 -- npm run benchmark:only", + "benchmark:only": "node benchmark/performance.js", + "benchmark:quick": "cross-env BENCHMARK_ITERATIONS=10 npm run benchmark:only" }, "types": "types/index.d.ts", "engines": { From 06f25ff31a5cc66f9cf3921f1d18de90228cdec1 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:28:07 +0100 Subject: [PATCH 16/50] ci: Fix performance step in CI missing permissions (#9917) --- .github/workflows/ci-performance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-performance.yml b/.github/workflows/ci-performance.yml index 4cde4d97b0..c65dd23dd2 100644 --- a/.github/workflows/ci-performance.yml +++ b/.github/workflows/ci-performance.yml @@ -1,6 +1,6 @@ name: ci-performance on: - pull_request: + pull_request_target: branches: - alpha - beta From a85ba199be1d42ce77e9ef6165cc367286fc8c9a Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:34:07 +0100 Subject: [PATCH 17/50] ci: Fix performance step in CI missing permissions (#9918) --- .github/workflows/ci-performance.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-performance.yml b/.github/workflows/ci-performance.yml index c65dd23dd2..4ae8c80d67 100644 --- a/.github/workflows/ci-performance.yml +++ b/.github/workflows/ci-performance.yml @@ -70,13 +70,19 @@ jobs: cat baseline.json continue-on-error: true + - name: Save baseline results to temp location + run: | + mkdir -p /tmp/benchmark-results + cp baseline.json /tmp/benchmark-results/ || echo '[]' > /tmp/benchmark-results/baseline.json + cp baseline-output.txt /tmp/benchmark-results/ || echo 'No baseline output' > /tmp/benchmark-results/baseline-output.txt + - name: Upload baseline results uses: actions/upload-artifact@v4 with: name: baseline-benchmark path: | - baseline.json - baseline-output.txt + /tmp/benchmark-results/baseline.json + /tmp/benchmark-results/baseline-output.txt retention-days: 7 - name: Checkout PR branch @@ -86,6 +92,11 @@ jobs: fetch-depth: 1 clean: true + - name: Restore baseline results + run: | + cp /tmp/benchmark-results/baseline.json ./ || echo '[]' > baseline.json + cp /tmp/benchmark-results/baseline-output.txt ./ || echo 'No baseline output' > baseline-output.txt + - name: Setup Node.js (PR) uses: actions/setup-node@v4 with: From b73ebac5c9b6edc375d19c48eebfb836a0a6057f Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:02:17 +0100 Subject: [PATCH 18/50] ci: Fix performance step in CI (#9921) --- .github/workflows/ci-performance.yml | 47 +++++++++++++---- CONTRIBUTING.md | 2 +- benchmark/performance.js | 79 ++++++++++++++++++---------- 3 files changed, 87 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci-performance.yml b/.github/workflows/ci-performance.yml index 4ae8c80d67..3fd2200d02 100644 --- a/.github/workflows/ci-performance.yml +++ b/.github/workflows/ci-performance.yml @@ -27,11 +27,31 @@ jobs: timeout-minutes: 30 steps: + - name: Checkout PR branch (for benchmark script) + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 1 + + - name: Save PR benchmark script + run: | + mkdir -p /tmp/pr-benchmark + cp -r benchmark /tmp/pr-benchmark/ || echo "No benchmark directory" + cp package.json /tmp/pr-benchmark/ || true + - name: Checkout base branch uses: actions/checkout@v4 with: ref: ${{ github.base_ref }} fetch-depth: 1 + clean: true + + - name: Restore PR benchmark script + run: | + if [ -d "/tmp/pr-benchmark/benchmark" ]; then + rm -rf benchmark + cp -r /tmp/pr-benchmark/benchmark . + fi - name: Setup Node.js uses: actions/setup-node@v4 @@ -47,17 +67,18 @@ jobs: - name: Run baseline benchmarks id: baseline + env: + NODE_ENV: production run: | - echo "Checking if benchmark script exists..." + echo "Running baseline benchmarks with CPU affinity (using PR's benchmark script)..." if [ ! -f "benchmark/performance.js" ]; then - echo "⚠️ Benchmark script not found in base branch - this is expected for new features" + echo "⚠️ Benchmark script not found - this is expected for new features" echo "Skipping baseline benchmark" echo '[]' > baseline.json - echo "Baseline: N/A (benchmark script not in base branch)" > baseline-output.txt + echo "Baseline: N/A (no benchmark script)" > baseline-output.txt exit 0 fi - echo "Running baseline benchmarks..." - npm run benchmark > baseline-output.txt 2>&1 || true + taskset -c 0 npm run benchmark > baseline-output.txt 2>&1 || npm run benchmark > baseline-output.txt 2>&1 || true echo "Benchmark command completed with exit code: $?" echo "Output file size: $(wc -c < baseline-output.txt) bytes" echo "--- Begin baseline-output.txt ---" @@ -111,9 +132,11 @@ jobs: - name: Run PR benchmarks id: pr-bench + env: + NODE_ENV: production run: | - echo "Running PR benchmarks..." - npm run benchmark > pr-output.txt 2>&1 || true + echo "Running PR benchmarks with CPU affinity..." + taskset -c 0 npm run benchmark > pr-output.txt 2>&1 || npm run benchmark > pr-output.txt 2>&1 || true echo "Benchmark command completed with exit code: $?" echo "Output file size: $(wc -c < pr-output.txt) bytes" echo "--- Begin pr-output.txt ---" @@ -224,13 +247,13 @@ jobs: const changeStr = change > 0 ? \`+\${change.toFixed(1)}%\` : \`\${change.toFixed(1)}%\`; let status = '✅'; - if (change > 20) { + if (change > 100) { status = '❌ Much Slower'; hasRegression = true; - } else if (change > 10) { + } else if (change > 50) { status = '⚠️ Slower'; hasRegression = true; - } else if (change < -10) { + } else if (change < -50) { status = '🚀 Faster'; hasImprovement = true; } @@ -281,7 +304,9 @@ jobs: echo "" >> comment.md echo "" >> comment.md echo "" >> comment.md - echo "*Benchmarks ran with ${BENCHMARK_ITERATIONS:-100} iterations per test on Node.js ${{ env.NODE_VERSION }}*" >> comment.md + echo "*Benchmarks ran with ${BENCHMARK_ITERATIONS:-10000} iterations per test on Node.js ${{ env.NODE_VERSION }} (production mode, CPU pinned)*" >> comment.md + echo "" >> comment.md + echo "> **Note:** Using 10k iterations with CPU affinity for measurement stability. Thresholds: ⚠️ >50%, ❌ >100%." >> comment.md - name: Comment PR with results if: github.event_name == 'pull_request' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f79caa4236..30050f87a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -341,7 +341,7 @@ Performance benchmarks are located in [`benchmark/performance.js`](benchmark/per 4. **Test locally**: Run the benchmarks locally to verify they work: ```bash npm run benchmark:quick # Quick test with 10 iterations - npm run benchmark # Full test with 100 iterations + npm run benchmark # Full test with 10,000 iterations ``` For new features where no baseline exists, the CI will establish new benchmarks that future PRs will be compared against. diff --git a/benchmark/performance.js b/benchmark/performance.js index 831a57db37..7021ed35b3 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -8,6 +8,8 @@ * Run with: npm run benchmark */ +/* eslint-disable no-console */ + const Parse = require('parse/node'); const { performance, PerformanceObserver } = require('perf_hooks'); const { MongoClient } = require('mongodb'); @@ -17,7 +19,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/parse_ const SERVER_URL = 'http://localhost:1337/parse'; const APP_ID = 'benchmark-app-id'; const MASTER_KEY = 'benchmark-master-key'; -const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '100', 10); +const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '10000', 10); // Parse Server instance let parseServer; @@ -39,6 +41,8 @@ async function initializeParseServer() { serverURL: SERVER_URL, silent: true, allowClientClassCreation: true, + logLevel: 'error', // Minimal logging for performance + verbose: false, }); app.use('/parse', parseServer.app); @@ -84,10 +88,18 @@ async function cleanupDatabase() { /** * Measure average time for an async operation over multiple iterations + * Uses warmup iterations, median metric, and outlier filtering for robustness */ async function measureOperation(name, operation, iterations = ITERATIONS) { + const warmupCount = Math.floor(iterations * 0.2); // 20% warmup iterations const times = []; + // Warmup phase - stabilize JIT compilation and caches + for (let i = 0; i < warmupCount; i++) { + await operation(); + } + + // Measurement phase for (let i = 0; i < iterations; i++) { const start = performance.now(); await operation(); @@ -95,22 +107,33 @@ async function measureOperation(name, operation, iterations = ITERATIONS) { times.push(end - start); } - // Calculate statistics + // Sort times for percentile calculations times.sort((a, b) => a - b); - const sum = times.reduce((acc, val) => acc + val, 0); - const mean = sum / times.length; - const p50 = times[Math.floor(times.length * 0.5)]; - const p95 = times[Math.floor(times.length * 0.95)]; - const p99 = times[Math.floor(times.length * 0.99)]; - const min = times[0]; - const max = times[times.length - 1]; + + // Filter outliers using Interquartile Range (IQR) method + const q1Index = Math.floor(times.length * 0.25); + const q3Index = Math.floor(times.length * 0.75); + const q1 = times[q1Index]; + const q3 = times[q3Index]; + const iqr = q3 - q1; + const lowerBound = q1 - 1.5 * iqr; + const upperBound = q3 + 1.5 * iqr; + + const filtered = times.filter(t => t >= lowerBound && t <= upperBound); + + // Calculate statistics on filtered data + const median = filtered[Math.floor(filtered.length * 0.5)]; + const p95 = filtered[Math.floor(filtered.length * 0.95)]; + const p99 = filtered[Math.floor(filtered.length * 0.99)]; + const min = filtered[0]; + const max = filtered[filtered.length - 1]; return { name, - value: mean, + value: median, // Use median (p50) as primary metric for stability in CI unit: 'ms', range: `${min.toFixed(2)} - ${max.toFixed(2)}`, - extra: `p50: ${p50.toFixed(2)}ms, p95: ${p95.toFixed(2)}ms, p99: ${p99.toFixed(2)}ms`, + extra: `p95: ${p95.toFixed(2)}ms, p99: ${p99.toFixed(2)}ms, n=${filtered.length}/${times.length}`, }; } @@ -274,15 +297,14 @@ async function benchmarkUserLogin() { * Run all benchmarks */ async function runBenchmarks() { - console.error('Starting Parse Server Performance Benchmarks...'); - console.error(`Iterations per benchmark: ${ITERATIONS}`); - console.error(''); + console.log('Starting Parse Server Performance Benchmarks...'); + console.log(`Iterations per benchmark: ${ITERATIONS}`); let server; try { // Initialize Parse Server - console.error('Initializing Parse Server...'); + console.log('Initializing Parse Server...'); server = await initializeParseServer(); // Wait for server to be ready @@ -291,43 +313,42 @@ async function runBenchmarks() { const results = []; // Run each benchmark with database cleanup - console.error('Running Object Create benchmark...'); + console.log('Running Object Create benchmark...'); await cleanupDatabase(); results.push(await benchmarkObjectCreate()); - console.error('Running Object Read benchmark...'); + console.log('Running Object Read benchmark...'); await cleanupDatabase(); results.push(await benchmarkObjectRead()); - console.error('Running Object Update benchmark...'); + console.log('Running Object Update benchmark...'); await cleanupDatabase(); results.push(await benchmarkObjectUpdate()); - console.error('Running Simple Query benchmark...'); + console.log('Running Simple Query benchmark...'); await cleanupDatabase(); results.push(await benchmarkSimpleQuery()); - console.error('Running Batch Save benchmark...'); + console.log('Running Batch Save benchmark...'); await cleanupDatabase(); results.push(await benchmarkBatchSave()); - console.error('Running User Signup benchmark...'); + console.log('Running User Signup benchmark...'); await cleanupDatabase(); results.push(await benchmarkUserSignup()); - console.error('Running User Login benchmark...'); + console.log('Running User Login benchmark...'); await cleanupDatabase(); results.push(await benchmarkUserLogin()); - // Output results in github-action-benchmark format + // Output results in github-action-benchmark format (stdout) console.log(JSON.stringify(results, null, 2)); - console.error(''); - console.error('Benchmarks completed successfully!'); - console.error(''); - console.error('Summary:'); + // Output summary to stderr for visibility + console.log('Benchmarks completed successfully!'); + console.log('Summary:'); results.forEach(result => { - console.error(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`); + console.log(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`); }); } catch (error) { @@ -342,7 +363,7 @@ async function runBenchmarks() { server.close(); } // Give some time for cleanup - setTimeout(() => process.exit(0), 1000); + setTimeout(() => process.exit(0), 10000); } } From 818824f6fc86ef61d5fa78e8765c21c76d6feba8 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:18:20 +0100 Subject: [PATCH 19/50] ci: Fix performance step in CI (#9922) --- .github/workflows/ci-performance.yml | 4 ++-- CONTRIBUTING.md | 2 +- benchmark/performance.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-performance.yml b/.github/workflows/ci-performance.yml index 3fd2200d02..438f5b1233 100644 --- a/.github/workflows/ci-performance.yml +++ b/.github/workflows/ci-performance.yml @@ -304,9 +304,9 @@ jobs: echo "" >> comment.md echo "" >> comment.md echo "" >> comment.md - echo "*Benchmarks ran with ${BENCHMARK_ITERATIONS:-10000} iterations per test on Node.js ${{ env.NODE_VERSION }} (production mode, CPU pinned)*" >> comment.md + echo "*Benchmarks ran with ${BENCHMARK_ITERATIONS:-1000} iterations per test on Node.js ${{ env.NODE_VERSION }} (production mode, CPU pinned)*" >> comment.md echo "" >> comment.md - echo "> **Note:** Using 10k iterations with CPU affinity for measurement stability. Thresholds: ⚠️ >50%, ❌ >100%." >> comment.md + echo "> **Note:** Using 1k iterations with CPU affinity for measurement stability. Thresholds: ⚠️ >50%, ❌ >100%." >> comment.md - name: Comment PR with results if: github.event_name == 'pull_request' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 30050f87a6..8e3db29efa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -341,7 +341,7 @@ Performance benchmarks are located in [`benchmark/performance.js`](benchmark/per 4. **Test locally**: Run the benchmarks locally to verify they work: ```bash npm run benchmark:quick # Quick test with 10 iterations - npm run benchmark # Full test with 10,000 iterations + npm run benchmark # Full test with 1,000 iterations ``` For new features where no baseline exists, the CI will establish new benchmarks that future PRs will be compared against. diff --git a/benchmark/performance.js b/benchmark/performance.js index 7021ed35b3..d6984560bb 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -19,7 +19,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/parse_ const SERVER_URL = 'http://localhost:1337/parse'; const APP_ID = 'benchmark-app-id'; const MASTER_KEY = 'benchmark-master-key'; -const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '10000', 10); +const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '1000', 10); // Parse Server instance let parseServer; From d94f348d86f998e3f7daf74128076f3892edf40b Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:21:58 +0100 Subject: [PATCH 20/50] ci: Cancel obsolete performance benchmark jobs (#9923) --- .github/workflows/ci-performance.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci-performance.yml b/.github/workflows/ci-performance.yml index 438f5b1233..782f4a3cd5 100644 --- a/.github/workflows/ci-performance.yml +++ b/.github/workflows/ci-performance.yml @@ -325,3 +325,6 @@ jobs: else echo "⚠️ Benchmark comparison not available" >> $GITHUB_STEP_SUMMARY fi +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true From 36e166cc813fbd68f1c195649c5c166f599215be Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 13:30:23 +0100 Subject: [PATCH 21/50] ci: Fix performance step in CI (#9925) --- .github/workflows/ci-performance.yml | 10 +++++----- CONTRIBUTING.md | 2 +- benchmark/performance.js | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-performance.yml b/.github/workflows/ci-performance.yml index 782f4a3cd5..78df157e89 100644 --- a/.github/workflows/ci-performance.yml +++ b/.github/workflows/ci-performance.yml @@ -247,13 +247,13 @@ jobs: const changeStr = change > 0 ? \`+\${change.toFixed(1)}%\` : \`\${change.toFixed(1)}%\`; let status = '✅'; - if (change > 100) { + if (change > 50) { status = '❌ Much Slower'; hasRegression = true; - } else if (change > 50) { + } else if (change > 25) { status = '⚠️ Slower'; hasRegression = true; - } else if (change < -50) { + } else if (change < -25) { status = '🚀 Faster'; hasImprovement = true; } @@ -304,9 +304,9 @@ jobs: echo "" >> comment.md echo "" >> comment.md echo "" >> comment.md - echo "*Benchmarks ran with ${BENCHMARK_ITERATIONS:-1000} iterations per test on Node.js ${{ env.NODE_VERSION }} (production mode, CPU pinned)*" >> comment.md + echo "*Benchmarks ran with ${BENCHMARK_ITERATIONS:-10000} iterations per test on Node.js ${{ env.NODE_VERSION }} (production mode, CPU pinned)*" >> comment.md echo "" >> comment.md - echo "> **Note:** Using 1k iterations with CPU affinity for measurement stability. Thresholds: ⚠️ >50%, ❌ >100%." >> comment.md + echo "> **Note:** Using 10k iterations with CPU affinity for measurement stability. Thresholds: ⚠️ >25%, ❌ >50%." >> comment.md - name: Comment PR with results if: github.event_name == 'pull_request' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e3db29efa..30050f87a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -341,7 +341,7 @@ Performance benchmarks are located in [`benchmark/performance.js`](benchmark/per 4. **Test locally**: Run the benchmarks locally to verify they work: ```bash npm run benchmark:quick # Quick test with 10 iterations - npm run benchmark # Full test with 1,000 iterations + npm run benchmark # Full test with 10,000 iterations ``` For new features where no baseline exists, the CI will establish new benchmarks that future PRs will be compared against. diff --git a/benchmark/performance.js b/benchmark/performance.js index d6984560bb..77e7f175ad 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -19,7 +19,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/parse_ const SERVER_URL = 'http://localhost:1337/parse'; const APP_ID = 'benchmark-app-id'; const MASTER_KEY = 'benchmark-master-key'; -const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '1000', 10); +const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '10000', 10); // Parse Server instance let parseServer; @@ -248,7 +248,7 @@ async function benchmarkBatchSave() { } await Parse.Object.saveAll(objects); - }, Math.floor(ITERATIONS / BATCH_SIZE)); // Fewer iterations for batch operations + }); } /** @@ -264,7 +264,7 @@ async function benchmarkUserSignup() { user.set('password', 'benchmark_password'); user.set('email', `benchmark${counter}@example.com`); await user.signUp(); - }, Math.floor(ITERATIONS / 10)); // Fewer iterations for user operations + }); } /** @@ -290,7 +290,7 @@ async function benchmarkUserLogin() { const userCreds = users[counter++ % users.length]; await Parse.User.logIn(userCreds.username, userCreds.password); await Parse.User.logOut(); - }, Math.floor(ITERATIONS / 10)); // Fewer iterations for user operations + }); } /** From 3cc8c1ae3e57c7e8640b3d4a973f9682b152fd2a Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 13:36:10 +0100 Subject: [PATCH 22/50] ci: Fix performance step in CI (#9926) --- .github/workflows/ci-performance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-performance.yml b/.github/workflows/ci-performance.yml index 78df157e89..e3afe3626f 100644 --- a/.github/workflows/ci-performance.yml +++ b/.github/workflows/ci-performance.yml @@ -326,5 +326,5 @@ jobs: echo "⚠️ Benchmark comparison not available" >> $GITHUB_STEP_SUMMARY fi concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true From 133660fb5289d5837f82ad1319e6010e0373e9ad Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 18:57:23 +0100 Subject: [PATCH 23/50] ci: Fix performance step in CI (#9927) --- .github/workflows/ci-performance.yml | 4 +--- benchmark/performance.js | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-performance.yml b/.github/workflows/ci-performance.yml index e3afe3626f..b080b668aa 100644 --- a/.github/workflows/ci-performance.yml +++ b/.github/workflows/ci-performance.yml @@ -304,9 +304,7 @@ jobs: echo "" >> comment.md echo "" >> comment.md echo "" >> comment.md - echo "*Benchmarks ran with ${BENCHMARK_ITERATIONS:-10000} iterations per test on Node.js ${{ env.NODE_VERSION }} (production mode, CPU pinned)*" >> comment.md - echo "" >> comment.md - echo "> **Note:** Using 10k iterations with CPU affinity for measurement stability. Thresholds: ⚠️ >25%, ❌ >50%." >> comment.md + echo "> **Note:** Thresholds: ⚠️ >25%, ❌ >50%." >> comment.md - name: Comment PR with results if: github.event_name == 'pull_request' diff --git a/benchmark/performance.js b/benchmark/performance.js index 77e7f175ad..64aa016df3 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -19,7 +19,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/parse_ const SERVER_URL = 'http://localhost:1337/parse'; const APP_ID = 'benchmark-app-id'; const MASTER_KEY = 'benchmark-master-key'; -const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '10000', 10); +const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '1000', 10); // Parse Server instance let parseServer; @@ -363,7 +363,7 @@ async function runBenchmarks() { server.close(); } // Give some time for cleanup - setTimeout(() => process.exit(0), 10000); + setTimeout(() => process.exit(0), 1000); } } From 52f7c89f3fa67ed2c8cbe41651cd11a9a7ef0a28 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Mon, 17 Nov 2025 01:14:29 +0100 Subject: [PATCH 24/50] ci: Fix performance step in CI (#9931) --- benchmark/MongoLatencyWrapper.js | 137 ++++++++++++ benchmark/performance.js | 372 +++++++++++++++++++++---------- 2 files changed, 388 insertions(+), 121 deletions(-) create mode 100644 benchmark/MongoLatencyWrapper.js diff --git a/benchmark/MongoLatencyWrapper.js b/benchmark/MongoLatencyWrapper.js new file mode 100644 index 0000000000..2b0480c1bc --- /dev/null +++ b/benchmark/MongoLatencyWrapper.js @@ -0,0 +1,137 @@ +/** + * MongoDB Latency Wrapper + * + * Utility to inject artificial latency into MongoDB operations for performance testing. + * This wrapper temporarily wraps MongoDB Collection methods to add delays before + * database operations execute. + * + * Usage: + * const { wrapMongoDBWithLatency } = require('./MongoLatencyWrapper'); + * + * // Before initializing Parse Server + * const unwrap = wrapMongoDBWithLatency(10); // 10ms delay + * + * // ... run benchmarks ... + * + * // Cleanup when done + * unwrap(); + */ + +const { Collection } = require('mongodb'); + +// Store original methods for restoration +const originalMethods = new Map(); + +/** + * Wrap a Collection method to add artificial latency + * @param {string} methodName - Name of the method to wrap + * @param {number} latencyMs - Delay in milliseconds + */ +function wrapMethod(methodName, latencyMs) { + if (!originalMethods.has(methodName)) { + originalMethods.set(methodName, Collection.prototype[methodName]); + } + + const originalMethod = originalMethods.get(methodName); + + Collection.prototype[methodName] = function (...args) { + // For methods that return cursors (like find, aggregate), we need to delay the execution + // but still return a cursor-like object + const result = originalMethod.apply(this, args); + + // Check if result has cursor methods (toArray, forEach, etc.) + if (result && typeof result.toArray === 'function') { + // Wrap cursor methods that actually execute the query + const originalToArray = result.toArray.bind(result); + result.toArray = function() { + // Wait for the original promise to settle, then delay the result + return originalToArray().then( + value => new Promise(resolve => setTimeout(() => resolve(value), latencyMs)), + error => new Promise((_, reject) => setTimeout(() => reject(error), latencyMs)) + ); + }; + return result; + } + + // For promise-returning methods, wrap the promise with delay + if (result && typeof result.then === 'function') { + // Wait for the original promise to settle, then delay the result + return result.then( + value => new Promise(resolve => setTimeout(() => resolve(value), latencyMs)), + error => new Promise((_, reject) => setTimeout(() => reject(error), latencyMs)) + ); + } + + // For synchronous methods, just add delay + return new Promise((resolve) => { + setTimeout(() => { + resolve(result); + }, latencyMs); + }); + }; +} + +/** + * Wrap MongoDB Collection methods with artificial latency + * @param {number} latencyMs - Delay in milliseconds to inject before each operation + * @returns {Function} unwrap - Function to restore original methods + */ +function wrapMongoDBWithLatency(latencyMs) { + if (typeof latencyMs !== 'number' || latencyMs < 0) { + throw new Error('latencyMs must be a non-negative number'); + } + + if (latencyMs === 0) { + // eslint-disable-next-line no-console + console.log('Latency is 0ms, skipping MongoDB wrapping'); + return () => {}; // No-op unwrap function + } + + // eslint-disable-next-line no-console + console.log(`Wrapping MongoDB operations with ${latencyMs}ms artificial latency`); + + // List of MongoDB Collection methods to wrap + const methodsToWrap = [ + 'find', + 'findOne', + 'countDocuments', + 'estimatedDocumentCount', + 'distinct', + 'aggregate', + 'insertOne', + 'insertMany', + 'updateOne', + 'updateMany', + 'replaceOne', + 'deleteOne', + 'deleteMany', + 'findOneAndUpdate', + 'findOneAndReplace', + 'findOneAndDelete', + 'createIndex', + 'createIndexes', + 'dropIndex', + 'dropIndexes', + 'drop', + ]; + + methodsToWrap.forEach(methodName => { + wrapMethod(methodName, latencyMs); + }); + + // Return unwrap function to restore original methods + return function unwrap() { + // eslint-disable-next-line no-console + console.log('Removing MongoDB latency wrapper, restoring original methods'); + + originalMethods.forEach((originalMethod, methodName) => { + Collection.prototype[methodName] = originalMethod; + }); + + originalMethods.clear(); + }; +} + +module.exports = { + wrapMongoDBWithLatency, +}; diff --git a/benchmark/performance.js b/benchmark/performance.js index 64aa016df3..4983d2bc7d 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -10,21 +10,28 @@ /* eslint-disable no-console */ +const core = require('@actions/core'); const Parse = require('parse/node'); const { performance, PerformanceObserver } = require('perf_hooks'); const { MongoClient } = require('mongodb'); +const { wrapMongoDBWithLatency } = require('./MongoLatencyWrapper'); // Configuration const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/parse_benchmark_test'; const SERVER_URL = 'http://localhost:1337/parse'; const APP_ID = 'benchmark-app-id'; const MASTER_KEY = 'benchmark-master-key'; -const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '1000', 10); +const ITERATIONS = process.env.BENCHMARK_ITERATIONS ? parseInt(process.env.BENCHMARK_ITERATIONS, 10) : undefined; +const LOG_ITERATIONS = false; // Parse Server instance let parseServer; let mongoClient; +// Logging helpers +const logInfo = message => core.info(message); +const logError = message => core.error(message); + /** * Initialize Parse Server for benchmarking */ @@ -87,54 +94,107 @@ async function cleanupDatabase() { } /** - * Measure average time for an async operation over multiple iterations - * Uses warmup iterations, median metric, and outlier filtering for robustness + * Reset Parse SDK to use the default server + */ +function resetParseServer() { + Parse.serverURL = SERVER_URL; +} + +/** + * Measure average time for an async operation over multiple iterations. + * @param {Object} options Measurement options. + * @param {string} options.name Name of the operation being measured. + * @param {Function} options.operation Async function to measure. + * @param {number} options.iterations Number of iterations to run; choose a value that is high + * enough to create reliable benchmark metrics with low variance but low enough to keep test + * duration reasonable around <=10 seconds. + * @param {boolean} [options.skipWarmup=false] Skip warmup phase. + * @param {number} [options.dbLatency] Artificial DB latency in milliseconds to apply during + * this benchmark. */ -async function measureOperation(name, operation, iterations = ITERATIONS) { - const warmupCount = Math.floor(iterations * 0.2); // 20% warmup iterations +async function measureOperation({ name, operation, iterations, skipWarmup = false, dbLatency }) { + // Override iterations if global ITERATIONS is set + iterations = ITERATIONS || iterations; + + // Determine warmup count (20% of iterations) + const warmupCount = skipWarmup ? 0 : Math.floor(iterations * 0.2); const times = []; - // Warmup phase - stabilize JIT compilation and caches - for (let i = 0; i < warmupCount; i++) { - await operation(); + // Apply artificial latency if specified + let unwrapLatency = null; + if (dbLatency !== undefined && dbLatency > 0) { + logInfo(`Applying ${dbLatency}ms artificial DB latency for this benchmark`); + unwrapLatency = wrapMongoDBWithLatency(dbLatency); } - // Measurement phase - for (let i = 0; i < iterations; i++) { - const start = performance.now(); - await operation(); - const end = performance.now(); - times.push(end - start); - } + try { + if (warmupCount > 0) { + logInfo(`Starting warmup phase of ${warmupCount} iterations...`); + const warmupStart = performance.now(); + for (let i = 0; i < warmupCount; i++) { + await operation(); + } + logInfo(`Warmup took: ${(performance.now() - warmupStart).toFixed(2)}ms`); + } + + // Measurement phase + logInfo(`Starting measurement phase of ${iterations} iterations...`); + const progressInterval = Math.ceil(iterations / 10); // Log every 10% + const measurementStart = performance.now(); + + for (let i = 0; i < iterations; i++) { + const start = performance.now(); + await operation(); + const end = performance.now(); + const duration = end - start; + times.push(duration); + + // Log progress every 10% or individual iterations if LOG_ITERATIONS is enabled + if (LOG_ITERATIONS) { + logInfo(`Iteration ${i + 1}: ${duration.toFixed(2)}ms`); + } else if ((i + 1) % progressInterval === 0 || i + 1 === iterations) { + const progress = Math.round(((i + 1) / iterations) * 100); + logInfo(`Progress: ${progress}%`); + } + } - // Sort times for percentile calculations - times.sort((a, b) => a - b); - - // Filter outliers using Interquartile Range (IQR) method - const q1Index = Math.floor(times.length * 0.25); - const q3Index = Math.floor(times.length * 0.75); - const q1 = times[q1Index]; - const q3 = times[q3Index]; - const iqr = q3 - q1; - const lowerBound = q1 - 1.5 * iqr; - const upperBound = q3 + 1.5 * iqr; - - const filtered = times.filter(t => t >= lowerBound && t <= upperBound); - - // Calculate statistics on filtered data - const median = filtered[Math.floor(filtered.length * 0.5)]; - const p95 = filtered[Math.floor(filtered.length * 0.95)]; - const p99 = filtered[Math.floor(filtered.length * 0.99)]; - const min = filtered[0]; - const max = filtered[filtered.length - 1]; - - return { - name, - value: median, // Use median (p50) as primary metric for stability in CI - unit: 'ms', - range: `${min.toFixed(2)} - ${max.toFixed(2)}`, - extra: `p95: ${p95.toFixed(2)}ms, p99: ${p99.toFixed(2)}ms, n=${filtered.length}/${times.length}`, - }; + logInfo(`Measurement took: ${(performance.now() - measurementStart).toFixed(2)}ms`); + + // Sort times for percentile calculations + times.sort((a, b) => a - b); + + // Filter outliers using Interquartile Range (IQR) method + const q1Index = Math.floor(times.length * 0.25); + const q3Index = Math.floor(times.length * 0.75); + const q1 = times[q1Index]; + const q3 = times[q3Index]; + const iqr = q3 - q1; + const lowerBound = q1 - 1.5 * iqr; + const upperBound = q3 + 1.5 * iqr; + + const filtered = times.filter(t => t >= lowerBound && t <= upperBound); + + // Calculate statistics on filtered data + const median = filtered[Math.floor(filtered.length * 0.5)]; + const p95 = filtered[Math.floor(filtered.length * 0.95)]; + const p99 = filtered[Math.floor(filtered.length * 0.99)]; + const min = filtered[0]; + const max = filtered[filtered.length - 1]; + + return { + name, + value: median, // Use median (p50) as primary metric for stability in CI + unit: 'ms', + range: `${min.toFixed(2)} - ${max.toFixed(2)}`, + extra: `p95: ${p95.toFixed(2)}ms, p99: ${p99.toFixed(2)}ms, n=${filtered.length}/${times.length}`, + }; + } finally { + // Remove latency wrapper if it was applied + if (unwrapLatency) { + unwrapLatency(); + logInfo('Removed artificial DB latency'); + } + } } /** @@ -143,13 +203,17 @@ async function measureOperation(name, operation, iterations = ITERATIONS) { async function benchmarkObjectCreate() { let counter = 0; - return measureOperation('Object Create', async () => { - const TestObject = Parse.Object.extend('BenchmarkTest'); - const obj = new TestObject(); - obj.set('testField', `test-value-${counter++}`); - obj.set('number', counter); - obj.set('boolean', true); - await obj.save(); + return measureOperation({ + name: 'Object Create', + iterations: 1_000, + operation: async () => { + const TestObject = Parse.Object.extend('BenchmarkTest'); + const obj = new TestObject(); + obj.set('testField', `test-value-${counter++}`); + obj.set('number', counter); + obj.set('boolean', true); + await obj.save(); + }, }); } @@ -161,7 +225,7 @@ async function benchmarkObjectRead() { const TestObject = Parse.Object.extend('BenchmarkTest'); const objects = []; - for (let i = 0; i < ITERATIONS; i++) { + for (let i = 0; i < 1_000; i++) { const obj = new TestObject(); obj.set('testField', `read-test-${i}`); objects.push(obj); @@ -171,9 +235,13 @@ async function benchmarkObjectRead() { let counter = 0; - return measureOperation('Object Read', async () => { - const query = new Parse.Query('BenchmarkTest'); - await query.get(objects[counter++ % objects.length].id); + return measureOperation({ + name: 'Object Read', + iterations: 1_000, + operation: async () => { + const query = new Parse.Query('BenchmarkTest'); + await query.get(objects[counter++ % objects.length].id); + }, }); } @@ -185,7 +253,7 @@ async function benchmarkObjectUpdate() { const TestObject = Parse.Object.extend('BenchmarkTest'); const objects = []; - for (let i = 0; i < ITERATIONS; i++) { + for (let i = 0; i < 1_000; i++) { const obj = new TestObject(); obj.set('testField', `update-test-${i}`); obj.set('counter', 0); @@ -196,11 +264,15 @@ async function benchmarkObjectUpdate() { let counter = 0; - return measureOperation('Object Update', async () => { - const obj = objects[counter++ % objects.length]; - obj.increment('counter'); - obj.set('lastUpdated', new Date()); - await obj.save(); + return measureOperation({ + name: 'Object Update', + iterations: 1_000, + operation: async () => { + const obj = objects[counter++ % objects.length]; + obj.increment('counter'); + obj.set('lastUpdated', new Date()); + await obj.save(); + }, }); } @@ -223,10 +295,14 @@ async function benchmarkSimpleQuery() { let counter = 0; - return measureOperation('Simple Query', async () => { - const query = new Parse.Query('BenchmarkTest'); - query.equalTo('category', counter++ % 10); - await query.find(); + return measureOperation({ + name: 'Simple Query', + iterations: 1_000, + operation: async () => { + const query = new Parse.Query('BenchmarkTest'); + query.equalTo('category', counter++ % 10); + await query.find(); + }, }); } @@ -236,18 +312,22 @@ async function benchmarkSimpleQuery() { async function benchmarkBatchSave() { const BATCH_SIZE = 10; - return measureOperation('Batch Save (10 objects)', async () => { - const TestObject = Parse.Object.extend('BenchmarkTest'); - const objects = []; - - for (let i = 0; i < BATCH_SIZE; i++) { - const obj = new TestObject(); - obj.set('batchField', `batch-${i}`); - obj.set('timestamp', new Date()); - objects.push(obj); - } + return measureOperation({ + name: 'Batch Save (10 objects)', + iterations: 1_000, + operation: async () => { + const TestObject = Parse.Object.extend('BenchmarkTest'); + const objects = []; + + for (let i = 0; i < BATCH_SIZE; i++) { + const obj = new TestObject(); + obj.set('batchField', `batch-${i}`); + obj.set('timestamp', new Date()); + objects.push(obj); + } - await Parse.Object.saveAll(objects); + await Parse.Object.saveAll(objects); + }, }); } @@ -257,13 +337,17 @@ async function benchmarkBatchSave() { async function benchmarkUserSignup() { let counter = 0; - return measureOperation('User Signup', async () => { - counter++; - const user = new Parse.User(); - user.set('username', `benchmark_user_${Date.now()}_${counter}`); - user.set('password', 'benchmark_password'); - user.set('email', `benchmark${counter}@example.com`); - await user.signUp(); + return measureOperation({ + name: 'User Signup', + iterations: 500, + operation: async () => { + counter++; + const user = new Parse.User(); + user.set('username', `benchmark_user_${Date.now()}_${counter}`); + user.set('password', 'benchmark_password'); + user.set('email', `benchmark${counter}@example.com`); + await user.signUp(); + }, }); } @@ -286,10 +370,66 @@ async function benchmarkUserLogin() { let counter = 0; - return measureOperation('User Login', async () => { - const userCreds = users[counter++ % users.length]; - await Parse.User.logIn(userCreds.username, userCreds.password); - await Parse.User.logOut(); + return measureOperation({ + name: 'User Login', + iterations: 500, + operation: async () => { + const userCreds = users[counter++ % users.length]; + await Parse.User.logIn(userCreds.username, userCreds.password); + await Parse.User.logOut(); + }, + }); +} + +/** + * Benchmark: Query with Include (Parallel Include Pointers) + */ +async function benchmarkQueryWithInclude() { + // Setup: Create nested object hierarchy + const Level2Class = Parse.Object.extend('Level2'); + const Level1Class = Parse.Object.extend('Level1'); + const RootClass = Parse.Object.extend('Root'); + + return measureOperation({ + name: 'Query with Include (2 levels)', + skipWarmup: true, + dbLatency: 50, + iterations: 100, + operation: async () => { + // Create 10 Level2 objects + const level2Objects = []; + for (let i = 0; i < 10; i++) { + const obj = new Level2Class(); + obj.set('name', `level2-${i}`); + obj.set('value', i); + level2Objects.push(obj); + } + await Parse.Object.saveAll(level2Objects); + + // Create 10 Level1 objects, each pointing to a Level2 object + const level1Objects = []; + for (let i = 0; i < 10; i++) { + const obj = new Level1Class(); + obj.set('name', `level1-${i}`); + obj.set('level2', level2Objects[i % level2Objects.length]); + level1Objects.push(obj); + } + await Parse.Object.saveAll(level1Objects); + + // Create 10 Root objects, each pointing to a Level1 object + const rootObjects = []; + for (let i = 0; i < 10; i++) { + const obj = new RootClass(); + obj.set('name', `root-${i}`); + obj.set('level1', level1Objects[i % level1Objects.length]); + rootObjects.push(obj); + } + await Parse.Object.saveAll(rootObjects); + + const query = new Parse.Query('Root'); + query.include('level1.level2'); + await query.find(); + }, }); } @@ -297,14 +437,13 @@ async function benchmarkUserLogin() { * Run all benchmarks */ async function runBenchmarks() { - console.log('Starting Parse Server Performance Benchmarks...'); - console.log(`Iterations per benchmark: ${ITERATIONS}`); + logInfo('Starting Parse Server Performance Benchmarks...'); let server; try { // Initialize Parse Server - console.log('Initializing Parse Server...'); + logInfo('Initializing Parse Server...'); server = await initializeParseServer(); // Wait for server to be ready @@ -312,47 +451,38 @@ async function runBenchmarks() { const results = []; - // Run each benchmark with database cleanup - console.log('Running Object Create benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkObjectCreate()); - - console.log('Running Object Read benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkObjectRead()); - - console.log('Running Object Update benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkObjectUpdate()); + // Define all benchmarks to run + const benchmarks = [ + { name: 'Object Create', fn: benchmarkObjectCreate }, + { name: 'Object Read', fn: benchmarkObjectRead }, + { name: 'Object Update', fn: benchmarkObjectUpdate }, + { name: 'Simple Query', fn: benchmarkSimpleQuery }, + { name: 'Batch Save', fn: benchmarkBatchSave }, + { name: 'User Signup', fn: benchmarkUserSignup }, + { name: 'User Login', fn: benchmarkUserLogin }, + { name: 'Query with Include', fn: benchmarkQueryWithInclude }, + ]; - console.log('Running Simple Query benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkSimpleQuery()); - - console.log('Running Batch Save benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkBatchSave()); - - console.log('Running User Signup benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkUserSignup()); - - console.log('Running User Login benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkUserLogin()); + // Run each benchmark with database cleanup + for (const benchmark of benchmarks) { + logInfo(`\nRunning benchmark '${benchmark.name}'...`); + resetParseServer(); + await cleanupDatabase(); + results.push(await benchmark.fn()); + } // Output results in github-action-benchmark format (stdout) - console.log(JSON.stringify(results, null, 2)); + logInfo(JSON.stringify(results, null, 2)); // Output summary to stderr for visibility - console.log('Benchmarks completed successfully!'); - console.log('Summary:'); + logInfo('Benchmarks completed successfully!'); + logInfo('Summary:'); results.forEach(result => { - console.log(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`); + logInfo(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`); }); } catch (error) { - console.error('Error running benchmarks:', error); + logError('Error running benchmarks:', error); process.exit(1); } finally { // Cleanup From dafea21eb39b0fdc2b52bb8a14f7b61e3f2b8d13 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Mon, 17 Nov 2025 15:42:49 +0100 Subject: [PATCH 25/50] perf: `Parse.Query.include` now fetches pointers at same level in parallel (#9861) --- .github/workflows/ci-performance.yml | 4 +- benchmark/performance.js | 213 +++++++++++++++++++-------- spec/RestQuery.spec.js | 82 +++++++++++ src/RestQuery.js | 61 +++++--- 4 files changed, 279 insertions(+), 81 deletions(-) diff --git a/.github/workflows/ci-performance.yml b/.github/workflows/ci-performance.yml index b080b668aa..c9cb055e13 100644 --- a/.github/workflows/ci-performance.yml +++ b/.github/workflows/ci-performance.yml @@ -70,7 +70,7 @@ jobs: env: NODE_ENV: production run: | - echo "Running baseline benchmarks with CPU affinity (using PR's benchmark script)..." + echo "Running baseline benchmarks..." if [ ! -f "benchmark/performance.js" ]; then echo "⚠️ Benchmark script not found - this is expected for new features" echo "Skipping baseline benchmark" @@ -135,7 +135,7 @@ jobs: env: NODE_ENV: production run: | - echo "Running PR benchmarks with CPU affinity..." + echo "Running PR benchmarks..." taskset -c 0 npm run benchmark > pr-output.txt 2>&1 || npm run benchmark > pr-output.txt 2>&1 || true echo "Benchmark command completed with exit code: $?" echo "Output file size: $(wc -c < pr-output.txt) bytes" diff --git a/benchmark/performance.js b/benchmark/performance.js index 4983d2bc7d..3518052434 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -200,11 +200,11 @@ async function measureOperation({ name, operation, iterations, skipWarmup = fals /** * Benchmark: Object Create */ -async function benchmarkObjectCreate() { +async function benchmarkObjectCreate(name) { let counter = 0; return measureOperation({ - name: 'Object Create', + name, iterations: 1_000, operation: async () => { const TestObject = Parse.Object.extend('BenchmarkTest'); @@ -220,7 +220,7 @@ async function benchmarkObjectCreate() { /** * Benchmark: Object Read (by ID) */ -async function benchmarkObjectRead() { +async function benchmarkObjectRead(name) { // Setup: Create test objects const TestObject = Parse.Object.extend('BenchmarkTest'); const objects = []; @@ -236,7 +236,7 @@ async function benchmarkObjectRead() { let counter = 0; return measureOperation({ - name: 'Object Read', + name, iterations: 1_000, operation: async () => { const query = new Parse.Query('BenchmarkTest'); @@ -248,7 +248,7 @@ async function benchmarkObjectRead() { /** * Benchmark: Object Update */ -async function benchmarkObjectUpdate() { +async function benchmarkObjectUpdate(name) { // Setup: Create test objects const TestObject = Parse.Object.extend('BenchmarkTest'); const objects = []; @@ -265,7 +265,7 @@ async function benchmarkObjectUpdate() { let counter = 0; return measureOperation({ - name: 'Object Update', + name, iterations: 1_000, operation: async () => { const obj = objects[counter++ % objects.length]; @@ -279,7 +279,7 @@ async function benchmarkObjectUpdate() { /** * Benchmark: Simple Query */ -async function benchmarkSimpleQuery() { +async function benchmarkSimpleQuery(name) { // Setup: Create test data const TestObject = Parse.Object.extend('BenchmarkTest'); const objects = []; @@ -296,7 +296,7 @@ async function benchmarkSimpleQuery() { let counter = 0; return measureOperation({ - name: 'Simple Query', + name, iterations: 1_000, operation: async () => { const query = new Parse.Query('BenchmarkTest'); @@ -309,11 +309,11 @@ async function benchmarkSimpleQuery() { /** * Benchmark: Batch Save (saveAll) */ -async function benchmarkBatchSave() { +async function benchmarkBatchSave(name) { const BATCH_SIZE = 10; return measureOperation({ - name: 'Batch Save (10 objects)', + name, iterations: 1_000, operation: async () => { const TestObject = Parse.Object.extend('BenchmarkTest'); @@ -334,11 +334,11 @@ async function benchmarkBatchSave() { /** * Benchmark: User Signup */ -async function benchmarkUserSignup() { +async function benchmarkUserSignup(name) { let counter = 0; return measureOperation({ - name: 'User Signup', + name, iterations: 500, operation: async () => { counter++; @@ -354,7 +354,7 @@ async function benchmarkUserSignup() { /** * Benchmark: User Login */ -async function benchmarkUserLogin() { +async function benchmarkUserLogin(name) { // Setup: Create test users const users = []; @@ -371,7 +371,7 @@ async function benchmarkUserLogin() { let counter = 0; return measureOperation({ - name: 'User Login', + name, iterations: 500, operation: async () => { const userCreds = users[counter++ % users.length]; @@ -382,52 +382,146 @@ async function benchmarkUserLogin() { } /** - * Benchmark: Query with Include (Parallel Include Pointers) + * Benchmark: Query with Include (Parallel Pointers) + * Tests the performance improvement when fetching multiple pointers at the same level. */ -async function benchmarkQueryWithInclude() { - // Setup: Create nested object hierarchy +async function benchmarkQueryWithIncludeParallel(name) { + const PointerAClass = Parse.Object.extend('PointerA'); + const PointerBClass = Parse.Object.extend('PointerB'); + const PointerCClass = Parse.Object.extend('PointerC'); + const RootClass = Parse.Object.extend('Root'); + + // Create pointer objects + const pointerAObjects = []; + for (let i = 0; i < 10; i++) { + const obj = new PointerAClass(); + obj.set('name', `pointerA-${i}`); + pointerAObjects.push(obj); + } + await Parse.Object.saveAll(pointerAObjects); + + const pointerBObjects = []; + for (let i = 0; i < 10; i++) { + const obj = new PointerBClass(); + obj.set('name', `pointerB-${i}`); + pointerBObjects.push(obj); + } + await Parse.Object.saveAll(pointerBObjects); + + const pointerCObjects = []; + for (let i = 0; i < 10; i++) { + const obj = new PointerCClass(); + obj.set('name', `pointerC-${i}`); + pointerCObjects.push(obj); + } + await Parse.Object.saveAll(pointerCObjects); + + // Create Root objects with multiple pointers at the same level + const rootObjects = []; + for (let i = 0; i < 10; i++) { + const obj = new RootClass(); + obj.set('name', `root-${i}`); + obj.set('pointerA', pointerAObjects[i % pointerAObjects.length]); + obj.set('pointerB', pointerBObjects[i % pointerBObjects.length]); + obj.set('pointerC', pointerCObjects[i % pointerCObjects.length]); + rootObjects.push(obj); + } + await Parse.Object.saveAll(rootObjects); + + return measureOperation({ + name, + skipWarmup: true, + dbLatency: 100, + iterations: 100, + operation: async () => { + const query = new Parse.Query('Root'); + // Include multiple pointers at the same level - should fetch in parallel + query.include(['pointerA', 'pointerB', 'pointerC']); + await query.find(); + }, + }); +} + +/** + * Benchmark: Query with Include (Nested Pointers with Parallel Leaf Nodes) + * Tests the PR's optimization for parallel fetching at each nested level. + * Pattern: p1.p2.p3, p1.p2.p4, p1.p2.p5 + * After fetching p2, we know the objectIds and can fetch p3, p4, p5 in parallel. + */ +async function benchmarkQueryWithIncludeNested(name) { + const Level3AClass = Parse.Object.extend('Level3A'); + const Level3BClass = Parse.Object.extend('Level3B'); + const Level3CClass = Parse.Object.extend('Level3C'); const Level2Class = Parse.Object.extend('Level2'); const Level1Class = Parse.Object.extend('Level1'); const RootClass = Parse.Object.extend('Root'); + // Create Level3 objects (leaf nodes) + const level3AObjects = []; + for (let i = 0; i < 10; i++) { + const obj = new Level3AClass(); + obj.set('name', `level3A-${i}`); + level3AObjects.push(obj); + } + await Parse.Object.saveAll(level3AObjects); + + const level3BObjects = []; + for (let i = 0; i < 10; i++) { + const obj = new Level3BClass(); + obj.set('name', `level3B-${i}`); + level3BObjects.push(obj); + } + await Parse.Object.saveAll(level3BObjects); + + const level3CObjects = []; + for (let i = 0; i < 10; i++) { + const obj = new Level3CClass(); + obj.set('name', `level3C-${i}`); + level3CObjects.push(obj); + } + await Parse.Object.saveAll(level3CObjects); + + // Create Level2 objects pointing to multiple Level3 objects + const level2Objects = []; + for (let i = 0; i < 10; i++) { + const obj = new Level2Class(); + obj.set('name', `level2-${i}`); + obj.set('level3A', level3AObjects[i % level3AObjects.length]); + obj.set('level3B', level3BObjects[i % level3BObjects.length]); + obj.set('level3C', level3CObjects[i % level3CObjects.length]); + level2Objects.push(obj); + } + await Parse.Object.saveAll(level2Objects); + + // Create Level1 objects pointing to Level2 + const level1Objects = []; + for (let i = 0; i < 10; i++) { + const obj = new Level1Class(); + obj.set('name', `level1-${i}`); + obj.set('level2', level2Objects[i % level2Objects.length]); + level1Objects.push(obj); + } + await Parse.Object.saveAll(level1Objects); + + // Create Root objects pointing to Level1 + const rootObjects = []; + for (let i = 0; i < 10; i++) { + const obj = new RootClass(); + obj.set('name', `root-${i}`); + obj.set('level1', level1Objects[i % level1Objects.length]); + rootObjects.push(obj); + } + await Parse.Object.saveAll(rootObjects); + return measureOperation({ - name: 'Query with Include (2 levels)', + name, skipWarmup: true, - dbLatency: 50, + dbLatency: 100, iterations: 100, operation: async () => { - // Create 10 Level2 objects - const level2Objects = []; - for (let i = 0; i < 10; i++) { - const obj = new Level2Class(); - obj.set('name', `level2-${i}`); - obj.set('value', i); - level2Objects.push(obj); - } - await Parse.Object.saveAll(level2Objects); - - // Create 10 Level1 objects, each pointing to a Level2 object - const level1Objects = []; - for (let i = 0; i < 10; i++) { - const obj = new Level1Class(); - obj.set('name', `level1-${i}`); - obj.set('level2', level2Objects[i % level2Objects.length]); - level1Objects.push(obj); - } - await Parse.Object.saveAll(level1Objects); - - // Create 10 Root objects, each pointing to a Level1 object - const rootObjects = []; - for (let i = 0; i < 10; i++) { - const obj = new RootClass(); - obj.set('name', `root-${i}`); - obj.set('level1', level1Objects[i % level1Objects.length]); - rootObjects.push(obj); - } - await Parse.Object.saveAll(rootObjects); - const query = new Parse.Query('Root'); - query.include('level1.level2'); + // After fetching level1.level2, the PR should fetch level3A, level3B, level3C in parallel + query.include(['level1.level2.level3A', 'level1.level2.level3B', 'level1.level2.level3C']); await query.find(); }, }); @@ -453,14 +547,15 @@ async function runBenchmarks() { // Define all benchmarks to run const benchmarks = [ - { name: 'Object Create', fn: benchmarkObjectCreate }, - { name: 'Object Read', fn: benchmarkObjectRead }, - { name: 'Object Update', fn: benchmarkObjectUpdate }, - { name: 'Simple Query', fn: benchmarkSimpleQuery }, - { name: 'Batch Save', fn: benchmarkBatchSave }, - { name: 'User Signup', fn: benchmarkUserSignup }, - { name: 'User Login', fn: benchmarkUserLogin }, - { name: 'Query with Include', fn: benchmarkQueryWithInclude }, + { name: 'Object.save (create)', fn: benchmarkObjectCreate }, + { name: 'Object.save (update)', fn: benchmarkObjectUpdate }, + { name: 'Object.saveAll (batch save)', fn: benchmarkBatchSave }, + { name: 'Query.get (by objectId)', fn: benchmarkObjectRead }, + { name: 'Query.find (simple query)', fn: benchmarkSimpleQuery }, + { name: 'User.signUp', fn: benchmarkUserSignup }, + { name: 'User.login', fn: benchmarkUserLogin }, + { name: 'Query.include (parallel pointers)', fn: benchmarkQueryWithIncludeParallel }, + { name: 'Query.include (nested pointers)', fn: benchmarkQueryWithIncludeNested }, ]; // Run each benchmark with database cleanup @@ -468,7 +563,7 @@ async function runBenchmarks() { logInfo(`\nRunning benchmark '${benchmark.name}'...`); resetParseServer(); await cleanupDatabase(); - results.push(await benchmark.fn()); + results.push(await benchmark.fn(benchmark.name)); } // Output results in github-action-benchmark format (stdout) diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 6fe3c0fa18..7b676da1ea 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -386,6 +386,88 @@ describe('rest query', () => { } ); }); + + it('battle test parallel include with 100 nested includes', async () => { + const RootObject = Parse.Object.extend('RootObject'); + const Level1Object = Parse.Object.extend('Level1Object'); + const Level2Object = Parse.Object.extend('Level2Object'); + + // Create 100 level2 objects (10 per level1 object) + const level2Objects = []; + for (let i = 0; i < 100; i++) { + const level2 = new Level2Object({ + index: i, + value: `level2_${i}`, + }); + level2Objects.push(level2); + } + await Parse.Object.saveAll(level2Objects); + + // Create 10 level1 objects, each with 10 pointers to level2 objects + const level1Objects = []; + for (let i = 0; i < 10; i++) { + const level1 = new Level1Object({ + index: i, + value: `level1_${i}`, + }); + // Set 10 pointer fields (level2_0 through level2_9) + for (let j = 0; j < 10; j++) { + level1.set(`level2_${j}`, level2Objects[i * 10 + j]); + } + level1Objects.push(level1); + } + await Parse.Object.saveAll(level1Objects); + + // Create 1 root object with 10 pointers to level1 objects + const rootObject = new RootObject({ + value: 'root', + }); + for (let i = 0; i < 10; i++) { + rootObject.set(`level1_${i}`, level1Objects[i]); + } + await rootObject.save(); + + // Build include paths: level1_0 through level1_9, and level1_0.level2_0 through level1_9.level2_9 + const includePaths = []; + for (let i = 0; i < 10; i++) { + includePaths.push(`level1_${i}`); + for (let j = 0; j < 10; j++) { + includePaths.push(`level1_${i}.level2_${j}`); + } + } + + // Query with all includes + const query = new Parse.Query(RootObject); + query.equalTo('objectId', rootObject.id); + for (const path of includePaths) { + query.include(path); + } + console.time('query.find'); + const results = await query.find(); + console.timeEnd('query.find'); + expect(results.length).toBe(1); + + const result = results[0]; + expect(result.id).toBe(rootObject.id); + + // Verify all 10 level1 objects are included + for (let i = 0; i < 10; i++) { + const level1Field = result.get(`level1_${i}`); + expect(level1Field).toBeDefined(); + expect(level1Field instanceof Parse.Object).toBe(true); + expect(level1Field.get('index')).toBe(i); + expect(level1Field.get('value')).toBe(`level1_${i}`); + + // Verify all 10 level2 objects are included for each level1 object + for (let j = 0; j < 10; j++) { + const level2Field = level1Field.get(`level2_${j}`); + expect(level2Field).toBeDefined(); + expect(level2Field instanceof Parse.Object).toBe(true); + expect(level2Field.get('index')).toBe(i * 10 + j); + expect(level2Field.get('value')).toBe(`level2_${i * 10 + j}`); + } + } + }); }); describe('RestQuery.each', () => { diff --git a/src/RestQuery.js b/src/RestQuery.js index dd226f249c..c48cecdb6f 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -856,31 +856,54 @@ _UnsafeRestQuery.prototype.handleExcludeKeys = function () { }; // Augments this.response with data at the paths provided in this.include. -_UnsafeRestQuery.prototype.handleInclude = function () { +_UnsafeRestQuery.prototype.handleInclude = async function () { if (this.include.length == 0) { return; } - var pathResponse = includePath( - this.config, - this.auth, - this.response, - this.include[0], - this.context, - this.restOptions - ); - if (pathResponse.then) { - return pathResponse.then(newResponse => { - this.response = newResponse; - this.include = this.include.slice(1); - return this.handleInclude(); + const indexedResults = this.response.results.reduce((indexed, result, i) => { + indexed[result.objectId] = i; + return indexed; + }, {}); + + // Build the execution tree + const executionTree = {} + this.include.forEach(path => { + let current = executionTree; + path.forEach((node) => { + if (!current[node]) { + current[node] = { + path, + children: {} + }; + } + current = current[node].children }); - } else if (this.include.length > 0) { - this.include = this.include.slice(1); - return this.handleInclude(); + }); + + const recursiveExecutionTree = async (treeNode) => { + const { path, children } = treeNode; + const pathResponse = includePath( + this.config, + this.auth, + this.response, + path, + this.context, + this.restOptions, + this, + ); + if (pathResponse.then) { + const newResponse = await pathResponse + newResponse.results.forEach(newObject => { + // We hydrate the root of each result with sub results + this.response.results[indexedResults[newObject.objectId]][path[0]] = newObject[path[0]]; + }) + } + return Promise.all(Object.values(children).map(recursiveExecutionTree)); } - return pathResponse; + await Promise.all(Object.values(executionTree).map(recursiveExecutionTree)); + this.include = [] }; //Returns a promise of a processed set of results @@ -1018,7 +1041,6 @@ function includePath(config, auth, response, path, context, restOptions = {}) { } else if (restOptions.readPreference) { includeRestOptions.readPreference = restOptions.readPreference; } - const queryPromises = Object.keys(pointersHash).map(async className => { const objectIds = Array.from(pointersHash[className]); let where; @@ -1057,7 +1079,6 @@ function includePath(config, auth, response, path, context, restOptions = {}) { } return replace; }, {}); - var resp = { results: replacePointers(response.results, path, replace), }; From 306c5fd8309d655e118bae54ce708db64a6eb86a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 17 Nov 2025 14:43:50 +0000 Subject: [PATCH 26/50] chore(release): 8.5.0-alpha.8 [skip ci] # [8.5.0-alpha.8](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.7...8.5.0-alpha.8) (2025-11-17) ### Performance Improvements * `Parse.Query.include` now fetches pointers at same level in parallel ([#9861](https://github.com/parse-community/parse-server/issues/9861)) ([dafea21](https://github.com/parse-community/parse-server/commit/dafea21eb39b0fdc2b52bb8a14f7b61e3f2b8d13)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 045b3ae5ab..5f436295f9 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.8](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.7...8.5.0-alpha.8) (2025-11-17) + + +### Performance Improvements + +* `Parse.Query.include` now fetches pointers at same level in parallel ([#9861](https://github.com/parse-community/parse-server/issues/9861)) ([dafea21](https://github.com/parse-community/parse-server/commit/dafea21eb39b0fdc2b52bb8a14f7b61e3f2b8d13)) + # [8.5.0-alpha.7](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.6...8.5.0-alpha.7) (2025-11-08) diff --git a/package-lock.json b/package-lock.json index 70067284a4..cb058f8179 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.7", + "version": "8.5.0-alpha.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.7", + "version": "8.5.0-alpha.8", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index fe043e8ee4..b289aa6796 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.7", + "version": "8.5.0-alpha.8", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 7d5e9fcf3ceb0abad8ab49c75bc26f521a0f1bde Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Mon, 17 Nov 2025 16:18:39 +0100 Subject: [PATCH 27/50] fix: Race condition can cause multiple Apollo server initializations under load (#9929) --- spec/ParseGraphQLServer.spec.js | 14 +++++++++ src/GraphQL/ParseGraphQLServer.js | 47 +++++++++++++++++++++---------- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index aee3575079..5031a9cdff 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -118,6 +118,20 @@ describe('ParseGraphQLServer', () => { expect(server3).not.toBe(server2); expect(server3).toBe(server4); }); + + it('should return same server reference when called 100 times in parallel', async () => { + parseGraphQLServer.server = undefined; + + // Call _getServer 100 times in parallel + const promises = Array.from({ length: 100 }, () => parseGraphQLServer._getServer()); + const servers = await Promise.all(promises); + + // All resolved servers should be the same reference + const firstServer = servers[0]; + servers.forEach((server, index) => { + expect(server).toBe(firstServer); + }); + }); }); describe('_getGraphQLOptions', () => { diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index bf7e14f7e2..231e44f5ef 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -97,21 +97,38 @@ class ParseGraphQLServer { if (schemaRef === newSchemaRef && this._server) { return this._server; } - const { schema, context } = await this._getGraphQLOptions(); - const apollo = new ApolloServer({ - csrfPrevention: { - // See https://www.apollographql.com/docs/router/configuration/csrf/ - // needed since we use graphql upload - requestHeaders: ['X-Parse-Application-Id'], - }, - introspection: this.config.graphQLPublicIntrospection, - plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection)], - schema, - }); - await apollo.start(); - this._server = expressMiddleware(apollo, { - context, - }); + // It means a parallel _getServer call is already in progress + if (this._schemaRefMutex === newSchemaRef) { + return this._server; + } + // Update the schema ref mutex to avoid parallel _getServer calls + this._schemaRefMutex = newSchemaRef; + const createServer = async () => { + try { + const { schema, context } = await this._getGraphQLOptions(); + const apollo = new ApolloServer({ + csrfPrevention: { + // See https://www.apollographql.com/docs/router/configuration/csrf/ + // needed since we use graphql upload + requestHeaders: ['X-Parse-Application-Id'], + }, + introspection: this.config.graphQLPublicIntrospection, + plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection)], + schema, + }); + await apollo.start(); + return expressMiddleware(apollo, { + context, + }); + } catch (e) { + // Reset all mutexes and forward the error + this._server = null; + this._schemaRefMutex = null; + throw e; + } + } + // Do not await so parallel request will wait the same promise ref + this._server = createServer(); return this._server; } From 8ff1d89ce25dc860957b996b1187499baae5c54d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 17 Nov 2025 15:19:31 +0000 Subject: [PATCH 28/50] chore(release): 8.5.0-alpha.9 [skip ci] # [8.5.0-alpha.9](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.8...8.5.0-alpha.9) (2025-11-17) ### Bug Fixes * Race condition can cause multiple Apollo server initializations under load ([#9929](https://github.com/parse-community/parse-server/issues/9929)) ([7d5e9fc](https://github.com/parse-community/parse-server/commit/7d5e9fcf3ceb0abad8ab49c75bc26f521a0f1bde)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 5f436295f9..2a695f95fe 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.9](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.8...8.5.0-alpha.9) (2025-11-17) + + +### Bug Fixes + +* Race condition can cause multiple Apollo server initializations under load ([#9929](https://github.com/parse-community/parse-server/issues/9929)) ([7d5e9fc](https://github.com/parse-community/parse-server/commit/7d5e9fcf3ceb0abad8ab49c75bc26f521a0f1bde)) + # [8.5.0-alpha.8](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.7...8.5.0-alpha.8) (2025-11-17) diff --git a/package-lock.json b/package-lock.json index cb058f8179..e95a0cc2f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.8", + "version": "8.5.0-alpha.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.8", + "version": "8.5.0-alpha.9", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index b289aa6796..3063cfc881 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.8", + "version": "8.5.0-alpha.9", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 7b9fa18f968ec084ea0b35dad2b5ba0451d59787 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:47:39 +0100 Subject: [PATCH 29/50] fix: Queries with object field `authData.provider.id` are incorrectly transformed to `_auth_data_provider.id` for custom classes (#9932) --- spec/MongoTransform.spec.js | 17 +++++++++++++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 60adb4b7f0..96a38f36dd 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -521,6 +521,23 @@ describe('parseObjectToMongoObjectForCreate', () => { expect(output.authData).toBe('random'); done(); }); + + it('should only transform authData.provider.id for _User class', () => { + // Test that for _User class, authData.facebook.id is transformed + const userInput = { + 'authData.facebook.id': '10000000000000001', + }; + const userOutput = transform.transformWhere('_User', userInput, { fields: {} }); + expect(userOutput['_auth_data_facebook.id']).toBe('10000000000000001'); + + // Test that for non-User classes, authData.facebook.id is NOT transformed + const customInput = { + 'authData.facebook.id': '10000000000000001', + }; + const customOutput = transform.transformWhere('SpamAlerts', customInput, { fields: {} }); + expect(customOutput['authData.facebook.id']).toBe('10000000000000001'); + expect(customOutput['_auth_data_facebook.id']).toBeUndefined(); + }); }); it('cannot have a custom field name beginning with underscore', done => { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index f78c972bdc..34481a090b 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -305,7 +305,7 @@ function transformQueryKeyValue(className, key, value, schema, count = false) { default: { // Other auth data const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); - if (authDataMatch) { + if (authDataMatch && className === '_User') { const provider = authDataMatch[1]; // Special-case auth data. return { key: `_auth_data_${provider}.id`, value }; From fcede163ca4ceb68ebc983b941ae2bcafb64fe91 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 17 Nov 2025 16:48:36 +0000 Subject: [PATCH 30/50] chore(release): 8.5.0-alpha.10 [skip ci] # [8.5.0-alpha.10](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.9...8.5.0-alpha.10) (2025-11-17) ### Bug Fixes * Queries with object field `authData.provider.id` are incorrectly transformed to `_auth_data_provider.id` for custom classes ([#9932](https://github.com/parse-community/parse-server/issues/9932)) ([7b9fa18](https://github.com/parse-community/parse-server/commit/7b9fa18f968ec084ea0b35dad2b5ba0451d59787)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 2a695f95fe..fc7cc0d3d3 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.10](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.9...8.5.0-alpha.10) (2025-11-17) + + +### Bug Fixes + +* Queries with object field `authData.provider.id` are incorrectly transformed to `_auth_data_provider.id` for custom classes ([#9932](https://github.com/parse-community/parse-server/issues/9932)) ([7b9fa18](https://github.com/parse-community/parse-server/commit/7b9fa18f968ec084ea0b35dad2b5ba0451d59787)) + # [8.5.0-alpha.9](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.8...8.5.0-alpha.9) (2025-11-17) diff --git a/package-lock.json b/package-lock.json index e95a0cc2f6..7230cab632 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.9", + "version": "8.5.0-alpha.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.9", + "version": "8.5.0-alpha.10", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 3063cfc881..4e73982a3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.9", + "version": "8.5.0-alpha.10", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 5e15403bc1b2fd840ada9c20b0141f26f6292cf4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 18:17:44 +0100 Subject: [PATCH 31/50] refactor: Bump js-yaml from 3.14.1 to 3.14.2 (#9933) --- package-lock.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7230cab632..ee14155bb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3380,9 +3380,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "dependencies": { "argparse": "^1.0.7", @@ -13313,9 +13313,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dependencies": { "argparse": "^2.0.1" }, @@ -25233,9 +25233,9 @@ } }, "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -32207,9 +32207,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "requires": { "argparse": "^2.0.1" } From c22cb0ae58e64cd0e4597ab9610d57a1155c44a2 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:43:32 +0100 Subject: [PATCH 32/50] fix: Deprecation warning logged at server launch for nested Parse Server option even if option is explicitly set (#9934) --- spec/Deprecator.spec.js | 25 ++++++++++++++++++ spec/Utils.spec.js | 51 ++++++++++++++++++++++++++++++++++++ src/Deprecator/Deprecator.js | 3 ++- src/Utils.js | 25 ++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/spec/Deprecator.spec.js b/spec/Deprecator.spec.js index 3af5d10c31..80e8632014 100644 --- a/spec/Deprecator.spec.js +++ b/spec/Deprecator.spec.js @@ -45,4 +45,29 @@ describe('Deprecator', () => { `DeprecationWarning: ${options.usage} is deprecated and will be removed in a future version. ${options.solution}` ); }); + + it('logs deprecation for nested option key with dot notation', async () => { + deprecations = [{ optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' }]; + + spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations); + const logger = require('../lib/logger').logger; + const logSpy = spyOn(logger, 'warn').and.callFake(() => {}); + + await reconfigureServer(); + expect(logSpy.calls.all()[0].args[0]).toEqual( + `DeprecationWarning: The Parse Server option '${deprecations[0].optionKey}' default will change to '${deprecations[0].changeNewDefault}' in a future version.` + ); + }); + + it('does not log deprecation for nested option key if option is set manually', async () => { + deprecations = [{ optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' }]; + + spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations); + const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {}); + const Config = require('../lib/Config'); + const config = Config.get('test'); + // Directly test scanParseServerOptions with nested option set + Deprecator.scanParseServerOptions({ databaseOptions: { allowPublicExplain: true } }); + expect(logSpy).not.toHaveBeenCalled(); + }); }); diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js index b1277c6bfe..2bbc5656a2 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -122,4 +122,55 @@ describe('Utils', () => { expect(result).toBe('{"name":"test","number":42,"nested":{"key":"value"}}'); }); }); + + describe('getNestedProperty', () => { + it('should get top-level property', () => { + const obj = { foo: 'bar' }; + expect(Utils.getNestedProperty(obj, 'foo')).toBe('bar'); + }); + + it('should get nested property with dot notation', () => { + const obj = { database: { options: { enabled: true } } }; + expect(Utils.getNestedProperty(obj, 'database.options.enabled')).toBe(true); + }); + + it('should return undefined for non-existent property', () => { + const obj = { foo: 'bar' }; + expect(Utils.getNestedProperty(obj, 'baz')).toBeUndefined(); + }); + + it('should return undefined for non-existent nested property', () => { + const obj = { database: { options: {} } }; + expect(Utils.getNestedProperty(obj, 'database.options.enabled')).toBeUndefined(); + }); + + it('should return undefined when path traverses non-object', () => { + const obj = { database: 'string' }; + expect(Utils.getNestedProperty(obj, 'database.options.enabled')).toBeUndefined(); + }); + + it('should return undefined for null object', () => { + expect(Utils.getNestedProperty(null, 'foo')).toBeUndefined(); + }); + + it('should return undefined for empty path', () => { + const obj = { foo: 'bar' }; + expect(Utils.getNestedProperty(obj, '')).toBeUndefined(); + }); + + it('should handle value of 0', () => { + const obj = { database: { timeout: 0 } }; + expect(Utils.getNestedProperty(obj, 'database.timeout')).toBe(0); + }); + + it('should handle value of false', () => { + const obj = { database: { enabled: false } }; + expect(Utils.getNestedProperty(obj, 'database.enabled')).toBe(false); + }); + + it('should handle value of empty string', () => { + const obj = { database: { name: '' } }; + expect(Utils.getNestedProperty(obj, 'database.name')).toBe(''); + }); + }); }); diff --git a/src/Deprecator/Deprecator.js b/src/Deprecator/Deprecator.js index 27033c946d..4744efbdd8 100644 --- a/src/Deprecator/Deprecator.js +++ b/src/Deprecator/Deprecator.js @@ -1,5 +1,6 @@ import logger from '../logger'; import Deprecations from './Deprecations'; +import Utils from '../Utils'; /** * The deprecator class. @@ -21,7 +22,7 @@ class Deprecator { const changeNewDefault = deprecation.changeNewDefault; // If default will change, only throw a warning if option is not set - if (changeNewDefault != null && options[optionKey] == null) { + if (changeNewDefault != null && Utils.getNestedProperty(options, optionKey) == null) { Deprecator._logOption({ optionKey, changeNewDefault, solution }); } } diff --git a/src/Utils.js b/src/Utils.js index f46e09de1e..cc00ac0bb7 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -444,6 +444,31 @@ class Utils { return value; }; } + + /** + * Gets a nested property value from an object using dot notation. + * @param {Object} obj The object to get the property from. + * @param {String} path The property path in dot notation, e.g. 'databaseOptions.allowPublicExplain'. + * @returns {any} The property value or undefined if not found. + * @example + * const obj = { database: { options: { enabled: true } } }; + * Utils.getNestedProperty(obj, 'database.options.enabled'); + * // Output: true + */ + static getNestedProperty(obj, path) { + if (!obj || !path) { + return undefined; + } + const keys = path.split('.'); + let current = obj; + for (const key of keys) { + if (current == null || typeof current !== 'object') { + return undefined; + } + current = current[key]; + } + return current; + } } module.exports = Utils; From 50650a362695e90ce3e6d94a139d32aa4c5ac765 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 17 Nov 2025 18:44:26 +0000 Subject: [PATCH 33/50] chore(release): 8.5.0-alpha.11 [skip ci] # [8.5.0-alpha.11](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.10...8.5.0-alpha.11) (2025-11-17) ### Bug Fixes * Deprecation warning logged at server launch for nested Parse Server option even if option is explicitly set ([#9934](https://github.com/parse-community/parse-server/issues/9934)) ([c22cb0a](https://github.com/parse-community/parse-server/commit/c22cb0ae58e64cd0e4597ab9610d57a1155c44a2)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index fc7cc0d3d3..bb33b00d43 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.11](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.10...8.5.0-alpha.11) (2025-11-17) + + +### Bug Fixes + +* Deprecation warning logged at server launch for nested Parse Server option even if option is explicitly set ([#9934](https://github.com/parse-community/parse-server/issues/9934)) ([c22cb0a](https://github.com/parse-community/parse-server/commit/c22cb0ae58e64cd0e4597ab9610d57a1155c44a2)) + # [8.5.0-alpha.10](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.9...8.5.0-alpha.10) (2025-11-17) diff --git a/package-lock.json b/package-lock.json index ee14155bb8..c25c1e638c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.10", + "version": "8.5.0-alpha.11", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.10", + "version": "8.5.0-alpha.11", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 4e73982a3a..3a768e5e08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.10", + "version": "8.5.0-alpha.11", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 94cee5bfafca10c914c73cf17fcdb627a9f0837b Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:57:28 +0100 Subject: [PATCH 34/50] feat: Add `beforePasswordResetRequest` hook (#9906) --- spec/CloudCode.spec.js | 164 ++++++++++++++++++++++++++++++++-- src/Routers/UsersRouter.js | 45 +++++++++- src/cloud-code/Parse.Cloud.js | 42 +++++++++ src/triggers.js | 6 +- 4 files changed, 247 insertions(+), 10 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 308c7731b8..b2aac473b2 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3307,19 +3307,19 @@ describe('afterFind hooks', () => { }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); expect(() => { Parse.Cloud.beforeLogin('SomeClass', () => { }); - }).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); + }).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); expect(() => { Parse.Cloud.afterLogin(() => { }); - }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); + }).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); expect(() => { Parse.Cloud.afterLogin('_User', () => { }); - }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); + }).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); expect(() => { Parse.Cloud.afterLogin(Parse.User, () => { }); - }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); + }).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); expect(() => { Parse.Cloud.afterLogin('SomeClass', () => { }); - }).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); + }).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); expect(() => { Parse.Cloud.afterLogout(() => { }); }).not.toThrow(); @@ -4656,3 +4656,157 @@ describe('sendEmail', () => { ); }); }); + +describe('beforePasswordResetRequest hook', () => { + it('should run beforePasswordResetRequest with valid user', async () => { + let hit = 0; + let sendPasswordResetEmailCalled = false; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => { + sendPasswordResetEmailCalled = true; + }, + sendMail: () => {}, + }; + + await reconfigureServer({ + appName: 'test', + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + + Parse.Cloud.beforePasswordResetRequest(req => { + hit++; + expect(req.object).toBeDefined(); + expect(req.object.get('email')).toEqual('test@example.com'); + expect(req.object.get('username')).toEqual('testuser'); + }); + + const user = new Parse.User(); + user.setUsername('testuser'); + user.setPassword('password'); + user.set('email', 'test@example.com'); + await user.signUp(); + + await Parse.User.requestPasswordReset('test@example.com'); + expect(hit).toBe(1); + expect(sendPasswordResetEmailCalled).toBe(true); + }); + + it('should be able to block password reset request if an error is thrown', async () => { + let hit = 0; + let sendPasswordResetEmailCalled = false; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => { + sendPasswordResetEmailCalled = true; + }, + sendMail: () => {}, + }; + + await reconfigureServer({ + appName: 'test', + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + + Parse.Cloud.beforePasswordResetRequest(req => { + hit++; + throw new Error('password reset blocked'); + }); + + const user = new Parse.User(); + user.setUsername('testuser'); + user.setPassword('password'); + user.set('email', 'test@example.com'); + await user.signUp(); + + try { + await Parse.User.requestPasswordReset('test@example.com'); + throw new Error('should not have sent password reset email.'); + } catch (e) { + expect(e.message).toBe('password reset blocked'); + } + expect(hit).toBe(1); + expect(sendPasswordResetEmailCalled).toBe(false); + }); + + it('should not run beforePasswordResetRequest if email does not exist', async () => { + let hit = 0; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => {}, + sendMail: () => {}, + }; + + await reconfigureServer({ + appName: 'test', + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + + Parse.Cloud.beforePasswordResetRequest(req => { + hit++; + }); + + await Parse.User.requestPasswordReset('nonexistent@example.com'); + + expect(hit).toBe(0); + }); + + it('should have expected data in request in beforePasswordResetRequest', async () => { + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => {}, + sendMail: () => {}, + }; + + await reconfigureServer({ + appName: 'test', + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + + const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE='; + const file = new Parse.File('myfile.txt', { base64 }); + await file.save(); + + Parse.Cloud.beforePasswordResetRequest(req => { + expect(req.object).toBeDefined(); + expect(req.object.get('email')).toBeDefined(); + expect(req.object.get('email')).toBe('test2@example.com'); + expect(req.object.get('file')).toBeDefined(); + expect(req.object.get('file')).toBeInstanceOf(Parse.File); + expect(req.object.get('file').name()).toContain('myfile.txt'); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + expect(req.config).toBeDefined(); + }); + + const user = new Parse.User(); + user.setUsername('testuser2'); + user.setPassword('password'); + user.set('email', 'test2@example.com'); + user.set('file', file); + await user.signUp(); + + await Parse.User.requestPasswordReset('test2@example.com'); + }); + + it('should validate that only _User class is allowed for beforePasswordResetRequest', () => { + expect(() => { + Parse.Cloud.beforePasswordResetRequest('SomeClass', () => { }); + }).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); + expect(() => { + Parse.Cloud.beforePasswordResetRequest(() => { }); + }).not.toThrow(); + expect(() => { + Parse.Cloud.beforePasswordResetRequest('_User', () => { }); + }).not.toThrow(); + expect(() => { + Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { }); + }).not.toThrow(); + }); +}); diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 4f38c60b6c..70745c2c69 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -12,6 +12,7 @@ import { Types as TriggerTypes, getRequestObject, resolveError, + inflate, } from '../triggers'; import { promiseEnsureIdempotency } from '../middlewares'; import RestWrite from '../RestWrite'; @@ -444,21 +445,59 @@ export class UsersRouter extends ClassesRouter { if (!email && !token) { throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email'); } + + let userResults = null; + let userData = null; + + // We can find the user using token if (token) { - const results = await req.config.database.find('_User', { + userResults = await req.config.database.find('_User', { _perishable_token: token, _perishable_token_expires_at: { $lt: Parse._encode(new Date()) }, }); - if (results && results[0] && results[0].email) { - email = results[0].email; + if (userResults?.length > 0) { + userData = userResults[0]; + if (userData.email) { + email = userData.email; + } + } + // Or using email if no token provided + } else if (typeof email === 'string') { + userResults = await req.config.database.find( + '_User', + { $or: [{ email }, { username: email, email: { $exists: false } }] }, + { limit: 1 }, + Auth.maintenance(req.config) + ); + if (userResults?.length > 0) { + userData = userResults[0]; } } + if (typeof email !== 'string') { throw new Parse.Error( Parse.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string' ); } + + if (userData) { + this._sanitizeAuthData(userData); + // Get files attached to user + await req.config.filesController.expandFilesInObject(req.config, userData); + + const user = inflate('_User', userData); + + await maybeRunTrigger( + TriggerTypes.beforePasswordResetRequest, + req.auth, + user, + null, + req.config, + req.info.context + ); + } + const userController = req.config.userController; try { await userController.sendPasswordResetEmail(email); diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index fa982de8f3..a057f8b3e6 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -349,6 +349,48 @@ ParseCloud.afterLogout = function (handler) { triggers.addTrigger(triggers.Types.afterLogout, className, handler, Parse.applicationId); }; +/** + * Registers the before password reset request function. + * + * **Available in Cloud Code only.** + * + * This function provides control in validating a password reset request + * before the reset email is sent. It is triggered after the user is found + * by email, but before the reset token is generated and the email is sent. + * + * Code example: + * + * ``` + * Parse.Cloud.beforePasswordResetRequest(request => { + * if (request.object.get('banned')) { + * throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User is banned.'); + * } + * }); + * ``` + * + * @method beforePasswordResetRequest + * @name Parse.Cloud.beforePasswordResetRequest + * @param {Function} func The function to run before a password reset request. This function can be async and should take one parameter a {@link Parse.Cloud.TriggerRequest}; + */ +ParseCloud.beforePasswordResetRequest = function (handler, validationHandler) { + let className = '_User'; + if (typeof handler === 'string' || isParseObjectConstructor(handler)) { + // validation will occur downstream, this is to maintain internal + // code consistency with the other hook types. + className = triggers.getClassName(handler); + handler = arguments[1]; + validationHandler = arguments.length >= 2 ? arguments[2] : null; + } + triggers.addTrigger(triggers.Types.beforePasswordResetRequest, className, handler, Parse.applicationId); + if (validationHandler && validationHandler.rateLimit) { + addRateLimit( + { requestPath: `/requestPasswordReset`, requestMethods: 'POST', ...validationHandler.rateLimit }, + Parse.applicationId, + true + ); + } +}; + /** * Registers an after save function. * diff --git a/src/triggers.js b/src/triggers.js index 26b107f062..e6abf20bb4 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -6,6 +6,7 @@ export const Types = { beforeLogin: 'beforeLogin', afterLogin: 'afterLogin', afterLogout: 'afterLogout', + beforePasswordResetRequest: 'beforePasswordResetRequest', beforeSave: 'beforeSave', afterSave: 'afterSave', beforeDelete: 'beforeDelete', @@ -58,10 +59,10 @@ function validateClassNameForTriggers(className, type) { // TODO: Allow proper documented way of using nested increment ops throw 'Only afterSave is allowed on _PushStatus'; } - if ((type === Types.beforeLogin || type === Types.afterLogin) && className !== '_User') { + if ((type === Types.beforeLogin || type === Types.afterLogin || type === Types.beforePasswordResetRequest) && className !== '_User') { // TODO: check if upstream code will handle `Error` instance rather // than this anti-pattern of throwing strings - throw 'Only the _User class is allowed for the beforeLogin and afterLogin triggers'; + throw 'Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'; } if (type === Types.afterLogout && className !== '_Session') { // TODO: check if upstream code will handle `Error` instance rather @@ -287,6 +288,7 @@ export function getRequestObject( triggerType === Types.afterDelete || triggerType === Types.beforeLogin || triggerType === Types.afterLogin || + triggerType === Types.beforePasswordResetRequest || triggerType === Types.afterFind ) { // Set a copy of the context on the request object. From 9ed9af48d1130896cc7ce6f970da6346103c425a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 19 Nov 2025 13:58:56 +0000 Subject: [PATCH 35/50] chore(release): 8.5.0-alpha.12 [skip ci] # [8.5.0-alpha.12](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.11...8.5.0-alpha.12) (2025-11-19) ### Features * Add `beforePasswordResetRequest` hook ([#9906](https://github.com/parse-community/parse-server/issues/9906)) ([94cee5b](https://github.com/parse-community/parse-server/commit/94cee5bfafca10c914c73cf17fcdb627a9f0837b)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index bb33b00d43..fb6d1640cf 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.12](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.11...8.5.0-alpha.12) (2025-11-19) + + +### Features + +* Add `beforePasswordResetRequest` hook ([#9906](https://github.com/parse-community/parse-server/issues/9906)) ([94cee5b](https://github.com/parse-community/parse-server/commit/94cee5bfafca10c914c73cf17fcdb627a9f0837b)) + # [8.5.0-alpha.11](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.10...8.5.0-alpha.11) (2025-11-17) diff --git a/package-lock.json b/package-lock.json index c25c1e638c..6019119b96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.11", + "version": "8.5.0-alpha.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.11", + "version": "8.5.0-alpha.12", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 3a768e5e08..4881e562a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.11", + "version": "8.5.0-alpha.12", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 69a925879ebbd4888854800f358d1a3ca8e19ace Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Sat, 22 Nov 2025 22:12:34 +0100 Subject: [PATCH 36/50] refactor: Add lint rules for no unused vars and unused import (#9940) --- benchmark/performance.js | 4 +--- ci/nodeEngineCheck.js | 2 +- eslint.config.js | 7 +++++++ package-lock.json | 24 ++++++++++++++++++++++ package.json | 1 + src/Adapters/Analytics/AnalyticsAdapter.js | 2 +- src/Adapters/Auth/AuthAdapter.js | 2 +- src/Adapters/Auth/apple.js | 2 +- src/Adapters/Auth/facebook.js | 2 +- src/Adapters/Cache/CacheAdapter.js | 2 +- src/Adapters/Email/MailAdapter.js | 2 +- src/Adapters/Files/FilesAdapter.js | 2 +- src/Adapters/Files/GridFSBucketAdapter.js | 2 +- src/Adapters/Logger/LoggerAdapter.js | 2 +- src/Adapters/Logger/WinstonLogger.js | 4 ++-- src/Adapters/PubSub/PubSubAdapter.js | 2 +- src/Adapters/Push/PushAdapter.js | 2 +- src/Adapters/WebSocketServer/WSAdapter.js | 2 +- src/Adapters/WebSocketServer/WSSAdapter.js | 2 +- src/Controllers/HooksController.js | 2 +- src/Controllers/index.js | 2 +- src/GraphQL/loaders/filesMutations.js | 2 +- src/Options/parsers.js | 2 +- src/Routers/ClassesRouter.js | 2 +- src/Routers/FilesRouter.js | 2 +- src/Routers/PagesRouter.js | 6 +++--- src/Utils.js | 2 +- src/batch.js | 2 +- src/cli/utils/runner.js | 2 +- src/middlewares.js | 6 +++--- src/password.js | 2 +- src/request.js | 2 +- 32 files changed, 66 insertions(+), 36 deletions(-) diff --git a/benchmark/performance.js b/benchmark/performance.js index 3518052434..c37ac09777 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -8,11 +8,9 @@ * Run with: npm run benchmark */ -/* eslint-disable no-console */ - const core = require('@actions/core'); const Parse = require('parse/node'); -const { performance, PerformanceObserver } = require('perf_hooks'); +const { performance } = require('node:perf_hooks'); const { MongoClient } = require('mongodb'); const { wrapMongoDBWithLatency } = require('./MongoLatencyWrapper'); diff --git a/ci/nodeEngineCheck.js b/ci/nodeEngineCheck.js index 4023353e17..65a806f760 100644 --- a/ci/nodeEngineCheck.js +++ b/ci/nodeEngineCheck.js @@ -86,7 +86,7 @@ class NodeEngineCheck { file: file, nodeVersion: version }); - } catch(e) { + } catch { // eslint-disable-next-line no-console console.log(`Ignoring file because it is not valid JSON: ${file}`); core.warning(`Ignoring file because it is not valid JSON: ${file}`); diff --git a/eslint.config.js b/eslint.config.js index d1cbac6e5e..3c5bd6806e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,6 +1,8 @@ const js = require("@eslint/js"); const babelParser = require("@babel/eslint-parser"); const globals = require("globals"); +const unusedImports = require("eslint-plugin-unused-imports"); + module.exports = [ { ignores: ["**/lib/**", "**/coverage/**", "**/out/**", "**/types/**"], @@ -19,8 +21,13 @@ module.exports = [ requireConfigFile: false, }, }, + plugins: { + "unused-imports": unusedImports, + }, rules: { indent: ["error", 2, { SwitchCase: 1 }], + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": "error", "linebreak-style": ["error", "unix"], "no-trailing-spaces": "error", "eol-last": "error", diff --git a/package-lock.json b/package-lock.json index 6019119b96..5adf5b2725 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,6 +82,7 @@ "deep-diff": "1.0.2", "eslint": "9.27.0", "eslint-plugin-expect-type": "0.6.2", + "eslint-plugin-unused-imports": "^4.3.0", "flow-bin": "0.271.0", "form-data": "4.0.4", "globals": "16.2.0", @@ -10246,6 +10247,22 @@ "typescript": ">=4" } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.3.0.tgz", + "integrity": "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -30154,6 +30171,13 @@ "get-tsconfig": "^4.8.1" } }, + "eslint-plugin-unused-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.3.0.tgz", + "integrity": "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==", + "dev": true, + "requires": {} + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/package.json b/package.json index 4881e562a7..261700884a 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "deep-diff": "1.0.2", "eslint": "9.27.0", "eslint-plugin-expect-type": "0.6.2", + "eslint-plugin-unused-imports": "4.3.0", "flow-bin": "0.271.0", "form-data": "4.0.4", "globals": "16.2.0", diff --git a/src/Adapters/Analytics/AnalyticsAdapter.js b/src/Adapters/Analytics/AnalyticsAdapter.js index e3cced14f5..380d869a5e 100644 --- a/src/Adapters/Analytics/AnalyticsAdapter.js +++ b/src/Adapters/Analytics/AnalyticsAdapter.js @@ -1,4 +1,4 @@ -/*eslint no-unused-vars: "off"*/ +/* eslint-disable unused-imports/no-unused-vars */ /** * @interface AnalyticsAdapter * @module Adapters diff --git a/src/Adapters/Auth/AuthAdapter.js b/src/Adapters/Auth/AuthAdapter.js index afc05d0bb2..ebcea4d3d8 100644 --- a/src/Adapters/Auth/AuthAdapter.js +++ b/src/Adapters/Auth/AuthAdapter.js @@ -1,4 +1,4 @@ -/*eslint no-unused-vars: "off"*/ +/* eslint-disable unused-imports/no-unused-vars */ /** * @interface ParseAuthResponse diff --git a/src/Adapters/Auth/apple.js b/src/Adapters/Auth/apple.js index 24502f4a55..7c9a845e95 100644 --- a/src/Adapters/Auth/apple.js +++ b/src/Adapters/Auth/apple.js @@ -63,7 +63,7 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => { let key; try { key = await authUtils.getSigningKey(client, keyId); - } catch (error) { + } catch { throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, `Unable to find matching key for Key ID: ${keyId}` diff --git a/src/Adapters/Auth/facebook.js b/src/Adapters/Auth/facebook.js index 273004ad62..d9b6589ab5 100644 --- a/src/Adapters/Auth/facebook.js +++ b/src/Adapters/Auth/facebook.js @@ -122,7 +122,7 @@ const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => { let key; try { key = await authUtils.getSigningKey(client, keyId); - } catch (error) { + } catch { throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, `Unable to find matching key for Key ID: ${keyId}` diff --git a/src/Adapters/Cache/CacheAdapter.js b/src/Adapters/Cache/CacheAdapter.js index 7632797e55..9a84f89ec6 100644 --- a/src/Adapters/Cache/CacheAdapter.js +++ b/src/Adapters/Cache/CacheAdapter.js @@ -1,4 +1,4 @@ -/*eslint no-unused-vars: "off"*/ +/* eslint-disable unused-imports/no-unused-vars */ /** * @interface * @memberof module:Adapters diff --git a/src/Adapters/Email/MailAdapter.js b/src/Adapters/Email/MailAdapter.js index 93069e2c27..10e77232b4 100644 --- a/src/Adapters/Email/MailAdapter.js +++ b/src/Adapters/Email/MailAdapter.js @@ -1,4 +1,4 @@ -/*eslint no-unused-vars: "off"*/ +/* eslint-disable unused-imports/no-unused-vars */ /** * @interface * @memberof module:Adapters diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js index f06c52df89..0e9b555853 100644 --- a/src/Adapters/Files/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -1,4 +1,4 @@ -/*eslint no-unused-vars: "off"*/ +/* eslint-disable unused-imports/no-unused-vars */ // Files Adapter // // Allows you to change the file storage mechanism. diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index b301d1c0c1..6820fc887f 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -171,7 +171,7 @@ export class GridFSBucketAdapter extends FilesAdapter { fileNamesNotRotated = fileNamesNotRotated.filter(function (value) { return value !== fileName; }); - } catch (err) { + } catch { continue; } } diff --git a/src/Adapters/Logger/LoggerAdapter.js b/src/Adapters/Logger/LoggerAdapter.js index 3853d5f480..957719be9b 100644 --- a/src/Adapters/Logger/LoggerAdapter.js +++ b/src/Adapters/Logger/LoggerAdapter.js @@ -1,4 +1,4 @@ -/*eslint no-unused-vars: "off"*/ +/* eslint-disable unused-imports/no-unused-vars */ /** * @interface * @memberof module:Adapters diff --git a/src/Adapters/Logger/WinstonLogger.js b/src/Adapters/Logger/WinstonLogger.js index fe28660056..886a1394f3 100644 --- a/src/Adapters/Logger/WinstonLogger.js +++ b/src/Adapters/Logger/WinstonLogger.js @@ -42,7 +42,7 @@ function configureTransports(options) { parseServerError.name = 'parse-server-error'; transports.push(parseServerError); } - } catch (e) { + } catch { /* */ } @@ -86,7 +86,7 @@ export function configureLogger({ } try { fs.mkdirSync(logsFolder); - } catch (e) { + } catch { /* */ } } diff --git a/src/Adapters/PubSub/PubSubAdapter.js b/src/Adapters/PubSub/PubSubAdapter.js index 728dff90e8..b68d9bee5d 100644 --- a/src/Adapters/PubSub/PubSubAdapter.js +++ b/src/Adapters/PubSub/PubSubAdapter.js @@ -1,4 +1,4 @@ -/*eslint no-unused-vars: "off"*/ +/* eslint-disable unused-imports/no-unused-vars */ /** * @interface * @memberof module:Adapters diff --git a/src/Adapters/Push/PushAdapter.js b/src/Adapters/Push/PushAdapter.js index fb0adbf469..d6862b00c3 100644 --- a/src/Adapters/Push/PushAdapter.js +++ b/src/Adapters/Push/PushAdapter.js @@ -1,5 +1,5 @@ +/* eslint-disable unused-imports/no-unused-vars */ // @flow -/*eslint no-unused-vars: "off"*/ // Push Adapter // // Allows you to change the push notification mechanism. diff --git a/src/Adapters/WebSocketServer/WSAdapter.js b/src/Adapters/WebSocketServer/WSAdapter.js index 5522dad365..db35d928bf 100644 --- a/src/Adapters/WebSocketServer/WSAdapter.js +++ b/src/Adapters/WebSocketServer/WSAdapter.js @@ -1,4 +1,4 @@ -/*eslint no-unused-vars: "off"*/ +/* eslint-disable unused-imports/no-unused-vars */ import { WSSAdapter } from './WSSAdapter'; const WebSocketServer = require('ws').Server; diff --git a/src/Adapters/WebSocketServer/WSSAdapter.js b/src/Adapters/WebSocketServer/WSSAdapter.js index a810c03f9d..0831828f3c 100644 --- a/src/Adapters/WebSocketServer/WSSAdapter.js +++ b/src/Adapters/WebSocketServer/WSSAdapter.js @@ -1,4 +1,4 @@ -/*eslint no-unused-vars: "off"*/ +/* eslint-disable unused-imports/no-unused-vars */ // WebSocketServer Adapter // // Adapter classes must implement the following functions: diff --git a/src/Controllers/HooksController.js b/src/Controllers/HooksController.js index fef01946f6..6497784754 100644 --- a/src/Controllers/HooksController.js +++ b/src/Controllers/HooksController.js @@ -225,7 +225,7 @@ function wrapToHTTPRequest(hook, key) { if (typeof body === 'string') { try { body = JSON.parse(body); - } catch (e) { + } catch { err = { error: 'Malformed response', code: -1, diff --git a/src/Controllers/index.js b/src/Controllers/index.js index abf0950640..9397dac561 100644 --- a/src/Controllers/index.js +++ b/src/Controllers/index.js @@ -217,7 +217,7 @@ export function getDatabaseAdapter(databaseURI, collectionPrefix, databaseOption try { const parsedURI = new URL(databaseURI); protocol = parsedURI.protocol ? parsedURI.protocol.toLowerCase() : null; - } catch (e) { + } catch { /* */ } switch (protocol) { diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js index 0a16a1c4a6..8439dfeb4f 100644 --- a/src/GraphQL/loaders/filesMutations.js +++ b/src/GraphQL/loaders/filesMutations.js @@ -38,7 +38,7 @@ const handleUpload = async (upload, config) => { res.on('end', () => { try { resolve(JSON.parse(data)); - } catch (e) { + } catch { reject(new Parse.Error(Parse.error, data)); } }); diff --git a/src/Options/parsers.js b/src/Options/parsers.js index 384b5494ef..3fdad89dc3 100644 --- a/src/Options/parsers.js +++ b/src/Options/parsers.js @@ -55,7 +55,7 @@ function moduleOrObjectParser(opt) { } try { return JSON.parse(opt); - } catch (e) { + } catch { /* */ } return opt; diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 8b6e447757..dd1ccb0a6b 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -149,7 +149,7 @@ export class ClassesRouter extends PromiseRouter { for (const [key, value] of _.entries(query)) { try { json[key] = JSON.parse(value); - } catch (e) { + } catch { json[key] = value; } } diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 5cb39abf47..a2ce85e222 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -310,7 +310,7 @@ export class FilesRouter { const data = await filesController.getMetadata(filename); res.status(200); res.json(data); - } catch (e) { + } catch { res.status(200); res.json({}); } diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 1ea3211684..74beec770c 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -432,7 +432,7 @@ export class PagesRouter extends PromiseRouter { let data; try { data = await this.readFile(path); - } catch (e) { + } catch { return this.notFound(); } @@ -474,7 +474,7 @@ export class PagesRouter extends PromiseRouter { let data; try { data = await this.readFile(path); - } catch (e) { + } catch { return this.notFound(); } @@ -517,7 +517,7 @@ export class PagesRouter extends PromiseRouter { try { const json = require(path.resolve('./', this.pagesConfig.localizationJsonPath)); this.jsonParameters = json; - } catch (e) { + } catch { throw errors.jsonFailedFileLoading; } } diff --git a/src/Utils.js b/src/Utils.js index cc00ac0bb7..0eca833552 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -82,7 +82,7 @@ class Utils { try { await fs.access(path); return true; - } catch (e) { + } catch { return false; } } diff --git a/src/batch.js b/src/batch.js index 80fa028cc6..ca1bea621a 100644 --- a/src/batch.js +++ b/src/batch.js @@ -13,7 +13,7 @@ function mountOnto(router) { function parseURL(urlString) { try { return new URL(urlString); - } catch (error) { + } catch { return undefined; } } diff --git a/src/cli/utils/runner.js b/src/cli/utils/runner.js index ed66cdfef8..6b18012af5 100644 --- a/src/cli/utils/runner.js +++ b/src/cli/utils/runner.js @@ -20,7 +20,7 @@ function logStartupOptions(options) { if (typeof value === 'object') { try { value = JSON.stringify(value); - } catch (e) { + } catch { if (value && value.constructor && value.constructor.name) { value = value.constructor.name; } diff --git a/src/middlewares.js b/src/middlewares.js index 93b16f3846..8ba3a9acff 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -79,7 +79,7 @@ export async function handleParseHeaders(req, res, next) { if (Object.prototype.toString.call(context) !== '[object Object]') { throw 'Context is not an object'; } - } catch (e) { + } catch { return malformedContext(req, res); } } @@ -126,7 +126,7 @@ export async function handleParseHeaders(req, res, next) { // to provide x-parse-app-id in header and parse a binary file will fail try { req.body = JSON.parse(req.body); - } catch (e) { + } catch { return invalidRequest(req, res); } fileViaJSON = true; @@ -173,7 +173,7 @@ export async function handleParseHeaders(req, res, next) { if (Object.prototype.toString.call(info.context) !== '[object Object]') { throw 'Context is not an object'; } - } catch (e) { + } catch { return malformedContext(req, res); } } diff --git a/src/password.js b/src/password.js index eebec14368..844f021937 100644 --- a/src/password.js +++ b/src/password.js @@ -8,7 +8,7 @@ try { hash: _bcrypt.hash, compare: _bcrypt.verify, }; -} catch (e) { +} catch { /* */ } diff --git a/src/request.js b/src/request.js index bc58ee40ac..d5754d9201 100644 --- a/src/request.js +++ b/src/request.js @@ -31,7 +31,7 @@ class HTTPResponse { if (!_data) { try { _data = JSON.parse(getText()); - } catch (e) { + } catch { /* */ } } From 38c9d2e3596933a7a03331fc7ea0b8cd3c77e7a4 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 23 Nov 2025 13:16:22 +0100 Subject: [PATCH 37/50] test: Add tests for `Parse.Query.includeAll` for circular and self-referencing pointers (#9936) --- spec/ParseQuery.spec.js | 85 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 2663e649ca..0aa173dc65 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -4670,6 +4670,91 @@ describe('Parse.Query testing', () => { .catch(done.fail); }); + it('includeAll handles circular pointer references', async () => { + // Create two objects that reference each other + const objA = new TestObject(); + const objB = new TestObject(); + + objA.set('name', 'Object A'); + objB.set('name', 'Object B'); + + // Save them first + await Parse.Object.saveAll([objA, objB]); + + // Create circular references: A -> B -> A + objA.set('ref', objB); + objB.set('ref', objA); + + await Parse.Object.saveAll([objA, objB]); + + // Query with includeAll + const query = new Parse.Query('TestObject'); + query.equalTo('objectId', objA.id); + query.includeAll(); + + const results = await query.find(); + + // Verify the object is returned + expect(results.length).toBe(1); + const resultA = results[0]; + expect(resultA.get('name')).toBe('Object A'); + + // Verify the immediate reference is included (1 level deep) + const refB = resultA.get('ref'); + expect(refB).toBeDefined(); + expect(refB.get('name')).toBe('Object B'); + + // Verify that includeAll only includes 1 level deep + // B's pointer back to A should exist as an object but without full data + const refBackToA = refB.get('ref'); + expect(refBackToA).toBeDefined(); + expect(refBackToA.id).toBe(objA.id); + + // The circular reference exists but is NOT fully populated + // (name field is undefined because it's not included at this depth) + expect(refBackToA.get('name')).toBeUndefined(); + + // Verify using toJSON that it's stored as a pointer + const refBackToAJSON = refB.toJSON().ref; + expect(refBackToAJSON).toBeDefined(); + expect(refBackToAJSON.__type).toBe('Pointer'); + expect(refBackToAJSON.className).toBe('TestObject'); + expect(refBackToAJSON.objectId).toBe(objA.id); + }); + + it('includeAll handles self-referencing pointer', async () => { + // Create an object that points to itself + const selfRef = new TestObject(); + selfRef.set('name', 'Self-Referencing'); + + await selfRef.save(); + + // Make it point to itself + selfRef.set('ref', selfRef); + await selfRef.save(); + + // Query with includeAll + const query = new Parse.Query('TestObject'); + query.equalTo('objectId', selfRef.id); + query.includeAll(); + + const results = await query.find(); + + // Verify the object is returned + expect(results.length).toBe(1); + const result = results[0]; + expect(result.get('name')).toBe('Self-Referencing'); + + // Verify the self-reference is included (since it's at the first level) + const ref = result.get('ref'); + expect(ref).toBeDefined(); + expect(ref.id).toBe(selfRef.id); + + // The self-reference should be fully populated at the first level + // because includeAll includes all pointer fields at the immediate level + expect(ref.get('name')).toBe('Self-Referencing'); + }); + it('select nested keys 2 level without include (issue #3185)', function (done) { const Foobar = new Parse.Object('Foobar'); const BarBaz = new Parse.Object('Barbaz'); From 50edb5ab4bb4a6ce474bfb7cf159d918933753b8 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Sun, 23 Nov 2025 13:51:42 +0100 Subject: [PATCH 38/50] fix: Server internal error details leaking in error messages returned to clients (#9937) --- package-lock.json | 2 +- spec/AudienceRouter.spec.js | 27 ++++++++-- spec/LogsRouter.spec.js | 6 ++- spec/ParseAPI.spec.js | 13 +++-- spec/ParseFile.spec.js | 19 +++++-- spec/ParseGlobalConfig.spec.js | 6 ++- spec/ParseGraphQLServer.spec.js | 35 +++++++++---- spec/ParseInstallation.spec.js | 9 +++- spec/ParseQuery.Aggregate.spec.js | 6 ++- spec/ParseUser.spec.js | 32 ++++++++++-- spec/RestQuery.spec.js | 12 +++-- spec/Schema.spec.js | 29 +++++++++-- spec/features.spec.js | 6 ++- spec/rest.spec.js | 68 +++++++++++++++++++------- spec/schemas.spec.js | 53 ++++++++++++++++---- spec/vulnerabilities.spec.js | 6 ++- src/Controllers/SchemaController.js | 9 ++-- src/Error.js | 44 +++++++++++++++++ src/GraphQL/loaders/schemaMutations.js | 11 +++-- src/GraphQL/loaders/usersQueries.js | 5 +- src/GraphQL/parseGraphQLUtils.js | 6 ++- src/RestQuery.js | 7 +-- src/RestWrite.js | 18 ++++--- src/Routers/ClassesRouter.js | 3 +- src/Routers/FilesRouter.js | 3 +- src/Routers/GlobalConfigRouter.js | 5 +- src/Routers/GraphQLRouter.js | 5 +- src/Routers/PurgeRouter.js | 5 +- src/Routers/PushRouter.js | 5 +- src/Routers/SchemasRouter.js | 13 ++--- src/Routers/UsersRouter.js | 10 ++-- src/SharedRest.js | 20 +++++--- src/TestUtils.js | 1 + src/middlewares.js | 11 ++--- src/rest.js | 5 +- 35 files changed, 390 insertions(+), 125 deletions(-) create mode 100644 src/Error.js diff --git a/package-lock.json b/package-lock.json index 5adf5b2725..c1d21724e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,7 +82,7 @@ "deep-diff": "1.0.2", "eslint": "9.27.0", "eslint-plugin-expect-type": "0.6.2", - "eslint-plugin-unused-imports": "^4.3.0", + "eslint-plugin-unused-imports": "4.3.0", "flow-bin": "0.271.0", "form-data": "4.0.4", "globals": "16.2.0", diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index 1525147a40..f6d6af9393 100644 --- a/spec/AudienceRouter.spec.js +++ b/spec/AudienceRouter.spec.js @@ -5,6 +5,13 @@ const request = require('../lib/request'); const AudiencesRouter = require('../lib/Routers/AudiencesRouter').AudiencesRouter; describe('AudiencesRouter', () => { + let loggerErrorSpy; + + beforeEach(() => { + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + }); + it('uses find condition from request.body', done => { const config = Config.get('test'); const androidAudienceRequest = { @@ -263,55 +270,65 @@ describe('AudiencesRouter', () => { }); it('should only create with master key', done => { + loggerErrorSpy.calls.reset(); Parse._request('POST', 'push_audiences', { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }), }).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); }); it('should only find with master key', done => { + loggerErrorSpy.calls.reset(); Parse._request('GET', 'push_audiences', {}).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); }); it('should only get with master key', done => { + loggerErrorSpy.calls.reset(); Parse._request('GET', `push_audiences/someId`, {}).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); }); it('should only update with master key', done => { + loggerErrorSpy.calls.reset(); Parse._request('PUT', `push_audiences/someId`, { name: 'My Audience 2', }).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); }); it('should only delete with master key', done => { + loggerErrorSpy.calls.reset(); Parse._request('DELETE', `push_audiences/someId`, {}).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); diff --git a/spec/LogsRouter.spec.js b/spec/LogsRouter.spec.js index b25ac25be5..d4b77baaa8 100644 --- a/spec/LogsRouter.spec.js +++ b/spec/LogsRouter.spec.js @@ -52,6 +52,9 @@ describe_only(() => { }); it('can check invalid master key of request', done => { + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ url: 'http://localhost:8378/1/scriptlog', headers: { @@ -61,7 +64,8 @@ describe_only(() => { }).then(fail, response => { const body = response.data; expect(response.status).toEqual(403); - expect(body.error).toEqual('unauthorized: master key is required'); + expect(body.error).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); }); }); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 6edfa79109..779a97c9f2 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -6,7 +6,7 @@ const request = require('../lib/request'); const Parse = require('parse/node'); const Config = require('../lib/Config'); const SchemaController = require('../lib/Controllers/SchemaController'); -const TestUtils = require('../lib/TestUtils'); +const { destroyAllDataPermanently } = require('../lib/TestUtils'); const userSchema = SchemaController.convertSchemaToAdapterSchema({ className: '_User', @@ -169,7 +169,7 @@ describe('miscellaneous', () => { } const config = Config.get('test'); // Remove existing data to clear out unique index - TestUtils.destroyAllDataPermanently() + destroyAllDataPermanently() .then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] })) .then(() => config.database.adapter.createClass('_User', userSchema)) .then(() => @@ -210,7 +210,7 @@ describe('miscellaneous', () => { it_id('d00f907e-41b9-40f6-8168-63e832199a8c')(it)('ensure that if people already have duplicate emails, they can still sign up new users', done => { const config = Config.get('test'); // Remove existing data to clear out unique index - TestUtils.destroyAllDataPermanently() + destroyAllDataPermanently() .then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] })) .then(() => config.database.adapter.createClass('_User', userSchema)) .then(() => @@ -1710,11 +1710,15 @@ describe('miscellaneous', () => { }); it('fail on purge all objects in class without master key', done => { + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', }; + loggerErrorSpy.calls.reset(); request({ method: 'DELETE', headers: headers, @@ -1724,7 +1728,8 @@ describe('miscellaneous', () => { fail('Should not succeed'); }) .catch(response => { - expect(response.data.error).toEqual('unauthorized: master key is required'); + expect(response.data.error).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); }); }); diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index d6539b7336..7e6f5e5080 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -13,6 +13,13 @@ for (let i = 0; i < str.length; i++) { } describe('Parse.File testing', () => { + let loggerErrorSpy; + + beforeEach(() => { + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + }); + describe('creating files', () => { it('works with Content-Type', done => { const headers = { @@ -146,6 +153,7 @@ describe('Parse.File testing', () => { const b = response.data; expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/); // missing X-Parse-Master-Key header + loggerErrorSpy.calls.reset(); request({ method: 'DELETE', headers: { @@ -156,8 +164,10 @@ describe('Parse.File testing', () => { }).then(fail, response => { const del_b = response.data; expect(response.status).toEqual(403); - expect(del_b.error).toMatch(/unauthorized/); + expect(del_b.error).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); // incorrect X-Parse-Master-Key header + loggerErrorSpy.calls.reset(); request({ method: 'DELETE', headers: { @@ -169,7 +179,8 @@ describe('Parse.File testing', () => { }).then(fail, response => { const del_b2 = response.data; expect(response.status).toEqual(403); - expect(del_b2.error).toMatch(/unauthorized/); + expect(del_b2.error).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); }); }); @@ -756,11 +767,13 @@ describe('Parse.File testing', () => { describe('getting files', () => { it('does not crash on file request with invalid app ID', async () => { + loggerErrorSpy.calls.reset(); const res1 = await request({ url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt', }).catch(e => e); expect(res1.status).toBe(403); - expect(res1.data).toEqual({ code: 119, error: 'Invalid application ID.' }); + expect(res1.data).toEqual({ code: 119, error: 'Permission denied' }); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid application ID.')); // Ensure server did not crash const res2 = await request({ url: 'http://localhost:8378/1/health' }); expect(res2.status).toEqual(200); diff --git a/spec/ParseGlobalConfig.spec.js b/spec/ParseGlobalConfig.spec.js index e6719433ff..1b3a9adc0d 100644 --- a/spec/ParseGlobalConfig.spec.js +++ b/spec/ParseGlobalConfig.spec.js @@ -220,6 +220,9 @@ describe('a GlobalConfig', () => { }); it('fail to update if master key is missing', done => { + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ method: 'PUT', url: 'http://localhost:8378/1/config', @@ -233,7 +236,8 @@ describe('a GlobalConfig', () => { }).then(fail, response => { const body = response.data; expect(response.status).toEqual(403); - expect(body.error).toEqual('unauthorized: master key is required'); + expect(body.error).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); }); }); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 5031a9cdff..aa57e973ef 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -47,6 +47,8 @@ function handleError(e) { describe('ParseGraphQLServer', () => { let parseServer; let parseGraphQLServer; + let loggerErrorSpy; + beforeEach(async () => { parseServer = await global.reconfigureServer({ @@ -58,6 +60,9 @@ describe('ParseGraphQLServer', () => { playgroundPath: '/playground', subscriptionsPath: '/subscriptions', }); + + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); }); describe('constructor', () => { @@ -3488,6 +3493,7 @@ describe('ParseGraphQLServer', () => { }); it('should require master key to create a new class', async () => { + loggerErrorSpy.calls.reset(); try { await apolloClient.mutate({ mutation: gql` @@ -3501,7 +3507,8 @@ describe('ParseGraphQLServer', () => { fail('should fail'); } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required'); + expect(e.graphQLErrors[0].message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); @@ -3858,6 +3865,7 @@ describe('ParseGraphQLServer', () => { handleError(e); } + loggerErrorSpy.calls.reset(); try { await apolloClient.mutate({ mutation: gql` @@ -3871,7 +3879,8 @@ describe('ParseGraphQLServer', () => { fail('should fail'); } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required'); + expect(e.graphQLErrors[0].message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); @@ -4083,6 +4092,7 @@ describe('ParseGraphQLServer', () => { handleError(e); } + loggerErrorSpy.calls.reset(); try { await apolloClient.mutate({ mutation: gql` @@ -4096,7 +4106,8 @@ describe('ParseGraphQLServer', () => { fail('should fail'); } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required'); + expect(e.graphQLErrors[0].message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); @@ -4124,6 +4135,7 @@ describe('ParseGraphQLServer', () => { }); it('should require master key to get an existing class', async () => { + loggerErrorSpy.calls.reset(); try { await apolloClient.query({ query: gql` @@ -4137,11 +4149,13 @@ describe('ParseGraphQLServer', () => { fail('should fail'); } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required'); + expect(e.graphQLErrors[0].message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); it('should require master key to find the existing classes', async () => { + loggerErrorSpy.calls.reset(); try { await apolloClient.query({ query: gql` @@ -4155,7 +4169,8 @@ describe('ParseGraphQLServer', () => { fail('should fail'); } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required'); + expect(e.graphQLErrors[0].message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); }); @@ -6081,7 +6096,7 @@ describe('ParseGraphQLServer', () => { } await expectAsync(createObject('GraphQLClass')).toBeRejectedWith( - jasmine.stringMatching('Permission denied for action create on class GraphQLClass') + jasmine.stringMatching('Permission denied') ); await expectAsync(createObject('PublicClass')).toBeResolved(); await expectAsync( @@ -6115,7 +6130,7 @@ describe('ParseGraphQLServer', () => { 'X-Parse-Session-Token': user4.getSessionToken(), }) ).toBeRejectedWith( - jasmine.stringMatching('Permission denied for action create on class GraphQLClass') + jasmine.stringMatching('Permission denied') ); await expectAsync( createObject('PublicClass', { @@ -7802,7 +7817,8 @@ describe('ParseGraphQLServer', () => { } catch (err) { const { graphQLErrors } = err; expect(graphQLErrors.length).toBe(1); - expect(graphQLErrors[0].message).toBe('Invalid session token'); + expect(graphQLErrors[0].message).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid session token')); } }); @@ -7840,7 +7856,8 @@ describe('ParseGraphQLServer', () => { } catch (err) { const { graphQLErrors } = err; expect(graphQLErrors.length).toBe(1); - expect(graphQLErrors[0].message).toBe('Invalid session token'); + expect(graphQLErrors[0].message).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid session token')); } }); }); diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index c03a727b4a..408e8fa7bf 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -157,6 +157,9 @@ describe('Installations', () => { }); it('should properly fail queying installations', done => { + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; const input = { @@ -166,6 +169,7 @@ describe('Installations', () => { rest .create(config, auth.nobody(config), '_Installation', input) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query(Parse.Installation); return query.find(); }) @@ -174,10 +178,11 @@ describe('Installations', () => { done(); }) .catch(error => { - expect(error.code).toBe(119); + expect(error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(error.message).toBe( - "Clients aren't allowed to perform the find operation on the installation collection." + 'Permission denied' ); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the find operation on the installation collection.")); done(); }); }); diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index d75658b19e..eb9c03ac4e 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -74,10 +74,14 @@ describe('Parse.Query Aggregate testing', () => { }); it('should only query aggregate with master key', done => { + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); Parse._request('GET', `aggregate/someClass`, {}).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index ba34fbf6e9..0380589057 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -13,6 +13,7 @@ const passwordCrypto = require('../lib/password'); const Config = require('../lib/Config'); const cryptoUtils = require('../lib/cryptoUtils'); + describe('allowExpiredAuthDataToken option', () => { it('should accept true value', async () => { await reconfigureServer({ allowExpiredAuthDataToken: true }); @@ -38,6 +39,12 @@ describe('allowExpiredAuthDataToken option', () => { }); describe('Parse.User testing', () => { + let loggerErrorSpy; + beforeEach(() => { + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + }); + it('user sign up class method', async done => { const user = await Parse.User.signUp('asdf', 'zxcv'); ok(user.getSessionToken()); @@ -2651,6 +2658,7 @@ describe('Parse.User testing', () => { const b = response.data; expect(b.results.length).toEqual(1); const objId = b.results[0].objectId; + loggerErrorSpy.calls.reset(); request({ method: 'DELETE', headers: { @@ -2661,7 +2669,9 @@ describe('Parse.User testing', () => { }).then(fail, response => { const b = response.data; expect(b.code).toEqual(209); - expect(b.error).toBe('Invalid session token'); + expect(b.error).toBe('Permission denied'); + + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid session token')); done(); }); }); @@ -3355,6 +3365,9 @@ describe('Parse.User testing', () => { sendMail: () => Promise.resolve(), }; + let logger; + let loggerErrorSpy; + const user = new Parse.User(); user.set({ username: 'hello', @@ -3369,9 +3382,12 @@ describe('Parse.User testing', () => { publicServerURL: 'http://localhost:8378/1', }) .then(() => { + logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); return user.signUp(); }) .then(() => { + loggerErrorSpy.calls.reset(); return Parse.User.current().set('emailVerified', true).save(); }) .then(() => { @@ -3379,7 +3395,9 @@ describe('Parse.User testing', () => { done(); }) .catch(err => { - expect(err.message).toBe("Clients aren't allowed to manually update email verification."); + expect(err.message).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to manually update email verification.")); + done(); }); }); @@ -4277,6 +4295,12 @@ describe('Security Advisory GHSA-8w3j-g983-8jh5', function () { }); describe('login as other user', () => { + let loggerErrorSpy; + beforeEach(() => { + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + }); + it('allows creating a session for another user with the master key', async done => { await Parse.User.signUp('some_user', 'some_password'); const userId = Parse.User.current().id; @@ -4376,6 +4400,7 @@ describe('login as other user', () => { const userId = Parse.User.current().id; await Parse.User.logOut(); + loggerErrorSpy.calls.reset(); try { await request({ method: 'POST', @@ -4393,7 +4418,8 @@ describe('login as other user', () => { done(); } catch (err) { expect(err.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(err.data.error).toBe('master key is required'); + expect(err.data.error).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('master key is required')); } const sessionsQuery = new Parse.Query(Parse.Session); diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 7b676da1ea..fb5370d759 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -5,7 +5,6 @@ const Config = require('../lib/Config'); const rest = require('../lib/rest'); const RestQuery = require('../lib/RestQuery'); const request = require('../lib/request'); - const querystring = require('querystring'); let config; @@ -155,9 +154,13 @@ describe('rest query', () => { }); it('query non-existent class when disabled client class creation', done => { + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + const customConfig = Object.assign({}, config, { allowClientClassCreation: false, }); + loggerErrorSpy.calls.reset(); rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}).then( () => { fail('Should throw an error'); @@ -165,9 +168,8 @@ describe('rest query', () => { }, err => { expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(err.message).toEqual( - 'This user is not allowed to access ' + 'non-existent class: ClientClassCreation' - ); + expect(err.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('This user is not allowed to access ' + 'non-existent class: ClientClassCreation')); done(); } ); @@ -243,7 +245,7 @@ describe('rest query', () => { expectAsync(new Parse.Query('Test').exists('zip').find()).toBeRejectedWith( new Parse.Error( Parse.Error.OPERATION_FORBIDDEN, - 'This user is not allowed to query zip on class Test' + 'Permission denied' ) ), ]); diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 2192678797..03c68276f8 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -20,8 +20,12 @@ const hasAllPODobject = () => { }; describe('SchemaController', () => { + let loggerErrorSpy; + beforeEach(() => { config = Config.get('test'); + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); }); it('can validate one object', done => { @@ -275,6 +279,7 @@ describe('SchemaController', () => { }) .then(results => { expect(results.length).toBe(1); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('Stuff'); return query.count(); }) @@ -283,7 +288,9 @@ describe('SchemaController', () => { fail('Class permissions should have rejected this query.'); }, err => { - expect(err.message).toEqual('Permission denied for action count on class Stuff.'); + expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action count on class Stuff')); done(); } ) @@ -1427,8 +1434,12 @@ describe('SchemaController', () => { }); describe('Class Level Permissions for requiredAuth', () => { + let loggerErrorSpy; + beforeEach(() => { config = Config.get('test'); + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); }); function createUser() { @@ -1453,6 +1464,7 @@ describe('Class Level Permissions for requiredAuth', () => { }); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('Stuff'); return query.find(); }) @@ -1462,7 +1474,8 @@ describe('Class Level Permissions for requiredAuth', () => { done(); }, e => { - expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); + expect(e.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.')); done(); } ); @@ -1551,6 +1564,7 @@ describe('Class Level Permissions for requiredAuth', () => { }); }) .then(() => { + loggerErrorSpy.calls.reset(); const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save(); @@ -1561,7 +1575,8 @@ describe('Class Level Permissions for requiredAuth', () => { done(); }, e => { - expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); + expect(e.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.')); done(); } ); @@ -1639,6 +1654,7 @@ describe('Class Level Permissions for requiredAuth', () => { const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save().then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('Stuff'); return query.get(stuff.id); }); @@ -1649,7 +1665,8 @@ describe('Class Level Permissions for requiredAuth', () => { done(); }, e => { - expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); + expect(e.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.')); done(); } ); @@ -1685,6 +1702,7 @@ describe('Class Level Permissions for requiredAuth', () => { }) .then(result => { expect(result.get('foo')).toEqual('bar'); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('Stuff'); return query.find(); }) @@ -1694,7 +1712,8 @@ describe('Class Level Permissions for requiredAuth', () => { done(); }, e => { - expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); + expect(e.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.')); done(); } ); diff --git a/spec/features.spec.js b/spec/features.spec.js index f138fe4cf6..201e01293d 100644 --- a/spec/features.spec.js +++ b/spec/features.spec.js @@ -20,6 +20,9 @@ describe('features', () => { }); it('requires the master key to get features', async done => { + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); try { await request({ url: 'http://localhost:8378/1/serverInfo', @@ -32,7 +35,8 @@ describe('features', () => { done.fail('The serverInfo request should be rejected without the master key'); } catch (error) { expect(error.status).toEqual(403); - expect(error.data.error).toEqual('unauthorized: master key is required'); + expect(error.data.error).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } }); diff --git a/spec/rest.spec.js b/spec/rest.spec.js index 1fff4fad59..4d8f40a982 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -11,9 +11,14 @@ let config; let database; describe('rest create', () => { + let loggerErrorSpy; + beforeEach(() => { config = Config.get('test'); database = config.database; + + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); }); it('handles _id', done => { @@ -317,6 +322,7 @@ describe('rest create', () => { const customConfig = Object.assign({}, config, { allowClientClassCreation: false, }); + loggerErrorSpy.calls.reset(); rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}).then( () => { fail('Should throw an error'); @@ -324,9 +330,8 @@ describe('rest create', () => { }, err => { expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(err.message).toEqual( - 'This user is not allowed to access ' + 'non-existent class: ClientClassCreation' - ); + expect(err.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('This user is not allowed to access ' + 'non-existent class: ClientClassCreation')); done(); } ); @@ -772,6 +777,7 @@ describe('rest create', () => { }); it('cannot get object in volatileClasses if not masterKey through pointer', async () => { + loggerErrorSpy.calls.reset(); const masterKeyOnlyClassObject = new Parse.Object('_PushStatus'); await masterKeyOnlyClassObject.save(null, { useMasterKey: true }); const obj2 = new Parse.Object('TestObject'); @@ -783,11 +789,13 @@ describe('rest create', () => { const query = new Parse.Query('TestObject'); query.include('pointer'); await expectAsync(query.get(obj2.id)).toBeRejectedWithError( - "Clients aren't allowed to perform the get operation on the _PushStatus collection." + 'Permission denied' ); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the get operation on the _PushStatus collection.")); }); it_id('3ce563bf-93aa-4d0b-9af9-c5fb246ac9fc')(it)('cannot get object in _GlobalConfig if not masterKey through pointer', async () => { + loggerErrorSpy.calls.reset(); await Parse.Config.save({ privateData: 'secret' }, { privateData: true }); const obj2 = new Parse.Object('TestObject'); obj2.set('globalConfigPointer', { @@ -799,8 +807,9 @@ describe('rest create', () => { const query = new Parse.Query('TestObject'); query.include('globalConfigPointer'); await expectAsync(query.get(obj2.id)).toBeRejectedWithError( - "Clients aren't allowed to perform the get operation on the _GlobalConfig collection." + 'Permission denied' ); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the get operation on the _GlobalConfig collection.")); }); it('locks down session', done => { @@ -945,7 +954,16 @@ describe('rest update', () => { }); describe('read-only masterKey', () => { + let loggerErrorSpy; + let logger; + + beforeEach(() => { + logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + }); + it('properly throws on rest.create, rest.update and rest.del', () => { + loggerErrorSpy.calls.reset(); const config = Config.get('test'); const readOnly = auth.readOnly(config); expect(() => { @@ -953,9 +971,10 @@ describe('read-only masterKey', () => { }).toThrow( new Parse.Error( Parse.Error.OPERATION_FORBIDDEN, - `read-only masterKey isn't allowed to perform the create operation.` + 'Permission denied' ) ); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to perform the create operation.")); expect(() => { rest.update(config, readOnly, 'AnObject', {}); }).toThrow(); @@ -968,6 +987,9 @@ describe('read-only masterKey', () => { await reconfigureServer({ readOnlyMasterKey: 'yolo-read-only', }); + // Need to be re required because reconfigureServer resets the logger + const logger2 = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger2, 'error').and.callThrough(); try { await request({ url: `${Parse.serverURL}/classes/MyYolo`, @@ -983,8 +1005,9 @@ describe('read-only masterKey', () => { } catch (res) { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe( - "read-only masterKey isn't allowed to perform the create operation." + 'Permission denied' ); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to perform the create operation.")); } await reconfigureServer(); }); @@ -1012,18 +1035,18 @@ describe('read-only masterKey', () => { }); it('should throw when trying to create RestWrite', () => { + loggerErrorSpy.calls.reset(); const config = Config.get('test'); expect(() => { new RestWrite(config, auth.readOnly(config)); }).toThrow( - new Parse.Error( - Parse.Error.OPERATION_FORBIDDEN, - 'Cannot perform a write operation when using readOnlyMasterKey' - ) + new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied') ); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Cannot perform a write operation when using readOnlyMasterKey")); }); it('should throw when trying to create schema', done => { + loggerErrorSpy.calls.reset(); request({ method: 'POST', url: `${Parse.serverURL}/schemas`, @@ -1037,12 +1060,14 @@ describe('read-only masterKey', () => { .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.data.error).toBe("read-only masterKey isn't allowed to create a schema."); + expect(res.data.error).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to create a schema.")); done(); }); }); it('should throw when trying to create schema with a name', done => { + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'POST', @@ -1056,12 +1081,14 @@ describe('read-only masterKey', () => { .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.data.error).toBe("read-only masterKey isn't allowed to create a schema."); + expect(res.data.error).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to create a schema.")); done(); }); }); it('should throw when trying to update schema', done => { + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'PUT', @@ -1075,12 +1102,14 @@ describe('read-only masterKey', () => { .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.data.error).toBe("read-only masterKey isn't allowed to update a schema."); + expect(res.data.error).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to update a schema.")); done(); }); }); it('should throw when trying to delete schema', done => { + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'DELETE', @@ -1094,12 +1123,14 @@ describe('read-only masterKey', () => { .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.data.error).toBe("read-only masterKey isn't allowed to delete a schema."); + expect(res.data.error).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to delete a schema.")); done(); }); }); it('should throw when trying to update the global config', done => { + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/config`, method: 'PUT', @@ -1113,12 +1144,14 @@ describe('read-only masterKey', () => { .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.data.error).toBe("read-only masterKey isn't allowed to update the config."); + expect(res.data.error).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to update the config.")); done(); }); }); it('should throw when trying to send push', done => { + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/push`, method: 'POST', @@ -1133,8 +1166,9 @@ describe('read-only masterKey', () => { .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe( - "read-only masterKey isn't allowed to send push notifications." + 'Permission denied' ); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to send push notifications.")); done(); }); }); diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 7891fa847e..5d92ef36e1 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -147,9 +147,14 @@ const masterKeyHeaders = { }; describe('schemas', () => { + let loggerErrorSpy; + beforeEach(async () => { await reconfigureServer(); config = Config.get('test'); + + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); }); it('requires the master key to get all schemas', done => { @@ -167,25 +172,29 @@ describe('schemas', () => { }); it('requires the master key to get one schema', done => { + loggerErrorSpy.calls.reset(); request({ url: 'http://localhost:8378/1/schemas/SomeSchema', json: true, headers: restKeyHeaders, }).then(fail, response => { expect(response.status).toEqual(403); - expect(response.data.error).toEqual('unauthorized: master key is required'); + expect(response.data.error).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("unauthorized: master key is required")); done(); }); }); it('asks for the master key if you use the rest key', done => { + loggerErrorSpy.calls.reset(); request({ url: 'http://localhost:8378/1/schemas', json: true, headers: restKeyHeaders, }).then(fail, response => { expect(response.status).toEqual(403); - expect(response.data.error).toEqual('unauthorized: master key is required'); + expect(response.data.error).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("unauthorized: master key is required")); done(); }); }); @@ -1826,6 +1835,7 @@ describe('schemas', () => { }, }, }).then(() => { + loggerErrorSpy.calls.reset(); const object = new Parse.Object('AClass'); object.set('hello', 'world'); return object.save().then( @@ -1834,7 +1844,9 @@ describe('schemas', () => { done(); }, err => { - expect(err.message).toEqual('Permission denied for action addField on class AClass.'); + expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action addField on class AClass')); done(); } ); @@ -2198,13 +2210,16 @@ describe('schemas', () => { }); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { fail('Use should hot be able to find!'); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2258,13 +2273,16 @@ describe('schemas', () => { }); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { fail('User should not be able to find!'); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2343,13 +2361,16 @@ describe('schemas', () => { }); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { fail('User should not be able to find!'); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2419,13 +2440,16 @@ describe('schemas', () => { }); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { fail('User should not be able to find!'); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2450,13 +2474,16 @@ describe('schemas', () => { ); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { fail('User should not be able to find!'); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2531,6 +2558,7 @@ describe('schemas', () => { return Parse.User.logIn('admin', 'admin'); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find(); }) @@ -2540,7 +2568,9 @@ describe('schemas', () => { return Promise.resolve(); }, err => { - expect(err.message).toEqual('Permission denied for action create on class AClass.'); + expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action create on class AClass')); return Promise.resolve(); } ) @@ -2548,6 +2578,7 @@ describe('schemas', () => { return Parse.User.logIn('user2', 'user2'); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find(); }) @@ -2557,7 +2588,9 @@ describe('schemas', () => { return Promise.resolve(); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index 0d66c0a135..f3aff6ef0b 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -13,9 +13,13 @@ describe('Vulnerabilities', () => { }); it('denies user creation with poisoned object ID', async () => { + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); await expectAsync( new Parse.User({ id: 'role:a', username: 'a', password: '123' }).save() - ).toBeRejectedWith(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.')); + ).toBeRejectedWith(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Invalid object ID.")); }); describe('existing sessions for users with poisoned object ID', () => { diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index fccadd23ce..70694925bf 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -20,6 +20,7 @@ import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; import SchemaCache from '../Adapters/Cache/SchemaCache'; import DatabaseController from './DatabaseController'; import Config from '../Config'; +import { createSanitizedError } from '../Error'; // @flow-disable-next import deepcopy from 'deepcopy'; import type { @@ -1403,12 +1404,12 @@ export default class SchemaController { if (perms['requiresAuthentication']) { // If aclGroup has * (public) if (!aclGroup || aclGroup.length == 0) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OBJECT_NOT_FOUND, 'Permission denied, user needs to be authenticated.' ); } else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OBJECT_NOT_FOUND, 'Permission denied, user needs to be authenticated.' ); @@ -1425,7 +1426,7 @@ export default class SchemaController { // Reject create when write lockdown if (permissionField == 'writeUserFields' && operation == 'create') { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, `Permission denied for action ${operation} on class ${className}.` ); @@ -1448,7 +1449,7 @@ export default class SchemaController { } } - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, `Permission denied for action ${operation} on class ${className}.` ); diff --git a/src/Error.js b/src/Error.js new file mode 100644 index 0000000000..4729965e1c --- /dev/null +++ b/src/Error.js @@ -0,0 +1,44 @@ +import defaultLogger from './logger'; + +/** + * Creates a sanitized error that hides detailed information from clients + * while logging the detailed message server-side. + * + * @param {number} errorCode - The Parse.Error code (e.g., Parse.Error.OPERATION_FORBIDDEN) + * @param {string} detailedMessage - The detailed error message to log server-side + * @returns {Parse.Error} A Parse.Error with sanitized message + */ +function createSanitizedError(errorCode, detailedMessage) { + // On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file + if (process.env.TESTING) { + defaultLogger.error('Sanitized error:', detailedMessage); + } else { + defaultLogger.error(detailedMessage); + } + + return new Parse.Error(errorCode, 'Permission denied'); +} + +/** + * Creates a sanitized error from a regular Error object + * Used for non-Parse.Error errors (e.g., Express errors) + * + * @param {number} statusCode - HTTP status code (e.g., 403) + * @param {string} detailedMessage - The detailed error message to log server-side + * @returns {Error} An Error with sanitized message + */ +function createSanitizedHttpError(statusCode, detailedMessage) { + // On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file + if (process.env.TESTING) { + defaultLogger.error('Sanitized error:', detailedMessage); + } else { + defaultLogger.error(detailedMessage); + } + + const error = new Error(); + error.status = statusCode; + error.message = 'Permission denied'; + return error; +} + +export { createSanitizedError, createSanitizedHttpError }; diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index ffb4d6523b..5dd8969bd9 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -6,6 +6,7 @@ import * as schemaTypes from './schemaTypes'; import { transformToParse, transformToGraphQL } from '../transformers/schemaFields'; import { enforceMasterKeyAccess } from '../parseGraphQLUtils'; import { getClass } from './schemaQueries'; +import { createSanitizedError } from '../../Error'; const load = parseGraphQLSchema => { const createClassMutation = mutationWithClientMutationId({ @@ -33,9 +34,9 @@ const load = parseGraphQLSchema => { enforceMasterKeyAccess(auth); if (auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to create a schema." + "read-only masterKey isn't allowed to create a schema.", ); } @@ -82,7 +83,7 @@ const load = parseGraphQLSchema => { enforceMasterKeyAccess(auth); if (auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to update a schema." ); @@ -133,9 +134,9 @@ const load = parseGraphQLSchema => { enforceMasterKeyAccess(auth); if (auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to delete a schema." + "read-only masterKey isn't allowed to delete a schema.", ); } diff --git a/src/GraphQL/loaders/usersQueries.js b/src/GraphQL/loaders/usersQueries.js index c64ce6b90d..a51e9553c0 100644 --- a/src/GraphQL/loaders/usersQueries.js +++ b/src/GraphQL/loaders/usersQueries.js @@ -4,11 +4,12 @@ import Parse from 'parse/node'; import rest from '../../rest'; import { extractKeysAndInclude } from './parseClassTypes'; import { Auth } from '../../Auth'; +import { createSanitizedError } from '../../Error'; const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) => { const { info, config } = context; if (!info || !info.sessionToken) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } const sessionToken = info.sessionToken; const selectedFields = getFieldNames(queryInfo) @@ -62,7 +63,7 @@ const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) = info.context ); if (!response.results || response.results.length == 0) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } else { const user = response.results[0]; return { diff --git a/src/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js index f1194784cb..1a0b266a36 100644 --- a/src/GraphQL/parseGraphQLUtils.js +++ b/src/GraphQL/parseGraphQLUtils.js @@ -1,9 +1,13 @@ import Parse from 'parse/node'; import { GraphQLError } from 'graphql'; +import { createSanitizedError } from '../Error'; export function enforceMasterKeyAccess(auth) { if (!auth.isMaster) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'unauthorized: master key is required'); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + 'unauthorized: master key is required', + ); } } diff --git a/src/RestQuery.js b/src/RestQuery.js index c48cecdb6f..b102caea23 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -7,6 +7,7 @@ const triggers = require('./triggers'); const { continueWhile } = require('parse/lib/node/promiseUtils'); const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL']; const { enforceRoleSecurity } = require('./SharedRest'); +const { createSanitizedError } = require('./Error'); // restOptions can include: // skip @@ -120,7 +121,7 @@ function _UnsafeRestQuery( if (!this.auth.isMaster) { if (this.className == '_Session') { if (!this.auth.user) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } this.restWhere = { $and: [ @@ -421,7 +422,7 @@ _UnsafeRestQuery.prototype.validateClientClassCreation = function () { .then(schemaController => schemaController.hasClass(this.className)) .then(hasClass => { if (hasClass !== true) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, 'This user is not allowed to access ' + 'non-existent class: ' + this.className ); @@ -800,7 +801,7 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () { ) || []; for (const key of protectedFields) { if (this.restWhere[key]) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, `This user is not allowed to query ${key} on class ${this.className}` ); diff --git a/src/RestWrite.js b/src/RestWrite.js index 41b6c23468..c8c9584fde 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -17,6 +17,7 @@ import RestQuery from './RestQuery'; import _ from 'lodash'; import logger from './logger'; import { requiredColumns } from './Controllers/SchemaController'; +import { createSanitizedError } from './Error'; // query and data are both provided in REST API format. So data // types are encoded by plain old objects. @@ -29,9 +30,9 @@ import { requiredColumns } from './Controllers/SchemaController'; // for the _User class. function RestWrite(config, auth, className, query, data, originalData, clientSDK, context, action) { if (auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - 'Cannot perform a write operation when using readOnlyMasterKey' + 'Cannot perform a write operation when using readOnlyMasterKey', ); } this.config = config; @@ -199,9 +200,9 @@ RestWrite.prototype.validateClientClassCreation = function () { .then(schemaController => schemaController.hasClass(this.className)) .then(hasClass => { if (hasClass !== true) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - 'This user is not allowed to access ' + 'non-existent class: ' + this.className + 'This user is not allowed to access non-existent class: ' + this.className, ); } }); @@ -566,7 +567,6 @@ RestWrite.prototype.handleAuthData = async function (authData) { // User found with provided authData if (results.length === 1) { - this.storage.authProvider = Object.keys(authData).join(','); const { hasMutatedAuthData, mutatedAuthData } = Auth.hasMutatedAuthData( @@ -660,8 +660,10 @@ RestWrite.prototype.checkRestrictedFields = async function () { } if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) { - const error = `Clients aren't allowed to manually update email verification.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + "Clients aren't allowed to manually update email verification." + ); } }; @@ -1450,7 +1452,7 @@ RestWrite.prototype.runDatabaseOperation = function () { } if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.SESSION_MISSING, `Cannot modify user ${this.query.objectId}.` ); diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index dd1ccb0a6b..09dc85022b 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -3,6 +3,7 @@ import rest from '../rest'; import _ from 'lodash'; import Parse from 'parse/node'; import { promiseEnsureIdempotency } from '../middlewares'; +import { createSanitizedError } from '../Error'; const ALLOWED_GET_QUERY_KEYS = [ 'keys', @@ -111,7 +112,7 @@ export class ClassesRouter extends PromiseRouter { typeof req.body?.objectId === 'string' && req.body.objectId.startsWith('role:') ) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.'); + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.'); } return rest.create( req.config, diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index a2ce85e222..e2b9271192 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -5,6 +5,7 @@ import Config from '../Config'; import logger from '../logger'; const triggers = require('../triggers'); const Utils = require('../Utils'); +import { createSanitizedError } from '../Error'; export class FilesRouter { expressRouter({ maxUploadSize = '20Mb' } = {}) { @@ -43,7 +44,7 @@ export class FilesRouter { const config = Config.get(req.params.appId); if (!config) { res.status(403); - const err = new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid application ID.'); + const err = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid application ID.'); res.json({ code: err.code, error: err.message }); return; } diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index 5a28b3bae1..4b107ee878 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -3,6 +3,7 @@ import Parse from 'parse/node'; import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import * as triggers from '../triggers'; +import { createSanitizedError } from '../Error'; const getConfigFromParams = params => { const config = new Parse.Config(); @@ -41,9 +42,9 @@ export class GlobalConfigRouter extends PromiseRouter { async updateGlobalConfig(req) { if (req.auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to update the config." + "read-only masterKey isn't allowed to update the config.", ); } const params = req.body.params || {}; diff --git a/src/Routers/GraphQLRouter.js b/src/Routers/GraphQLRouter.js index d472ac9df5..d785afbdf2 100644 --- a/src/Routers/GraphQLRouter.js +++ b/src/Routers/GraphQLRouter.js @@ -1,6 +1,7 @@ import Parse from 'parse/node'; import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; +import { createSanitizedError } from '../Error'; const GraphQLConfigPath = '/graphql-config'; @@ -14,9 +15,9 @@ export class GraphQLRouter extends PromiseRouter { async updateGraphQLConfig(req) { if (req.auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to update the GraphQL config." + "read-only masterKey isn't allowed to update the GraphQL config.", ); } const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {}); diff --git a/src/Routers/PurgeRouter.js b/src/Routers/PurgeRouter.js index 3195d134af..7b992a48e2 100644 --- a/src/Routers/PurgeRouter.js +++ b/src/Routers/PurgeRouter.js @@ -1,13 +1,14 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import Parse from 'parse/node'; +import { createSanitizedError } from '../Error'; export class PurgeRouter extends PromiseRouter { handlePurge(req) { if (req.auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to purge a schema." + "read-only masterKey isn't allowed to purge a schema.", ); } return req.config.database diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js index 1c1c8f3b5f..123677f138 100644 --- a/src/Routers/PushRouter.js +++ b/src/Routers/PushRouter.js @@ -1,6 +1,7 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import { Parse } from 'parse/node'; +import { createSanitizedError } from '../Error'; export class PushRouter extends PromiseRouter { mountRoutes() { @@ -9,9 +10,9 @@ export class PushRouter extends PromiseRouter { static handlePOST(req) { if (req.auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to send push notifications." + "read-only masterKey isn't allowed to send push notifications.", ); } const pushController = req.config.pushController; diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index 0a42123af7..ff55711a69 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -5,6 +5,7 @@ var Parse = require('parse/node').Parse, import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; +import { createSanitizedError } from '../Error'; function classNameMismatchResponse(bodyClass, pathClass) { throw new Parse.Error( @@ -72,9 +73,9 @@ export const internalUpdateSchema = async (className, body, config) => { async function createSchema(req) { checkIfDefinedSchemasIsUsed(req); if (req.auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to create a schema." + "read-only masterKey isn't allowed to create a schema.", ); } if (req.params.className && req.body?.className) { @@ -94,9 +95,9 @@ async function createSchema(req) { function modifySchema(req) { checkIfDefinedSchemasIsUsed(req); if (req.auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to update a schema." + "read-only masterKey isn't allowed to update a schema.", ); } if (req.body?.className && req.body.className != req.params.className) { @@ -109,9 +110,9 @@ function modifySchema(req) { const deleteSchema = req => { if (req.auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to delete a schema." + "read-only masterKey isn't allowed to delete a schema.", ); } if (!SchemaController.classNameIsValid(req.params.className)) { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 70745c2c69..f50f9608d2 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -17,6 +17,7 @@ import { import { promiseEnsureIdempotency } from '../middlewares'; import RestWrite from '../RestWrite'; import { logger } from '../logger'; +import { createSanitizedError } from '../Error'; export class UsersRouter extends ClassesRouter { className() { @@ -171,7 +172,7 @@ export class UsersRouter extends ClassesRouter { handleMe(req) { if (!req.info || !req.info.sessionToken) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } const sessionToken = req.info.sessionToken; return rest @@ -186,7 +187,7 @@ export class UsersRouter extends ClassesRouter { ) .then(response => { if (!response.results || response.results.length == 0 || !response.results[0].user) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } else { const user = response.results[0].user; // Send token back on the login, because SDKs expect that. @@ -334,7 +335,10 @@ export class UsersRouter extends ClassesRouter { */ async handleLogInAs(req) { if (!req.auth.isMaster) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'master key is required'); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + 'master key is required', + ); } const userId = req.body?.userId || req.query.userId; diff --git a/src/SharedRest.js b/src/SharedRest.js index 0b4a07c320..1d342b595a 100644 --- a/src/SharedRest.js +++ b/src/SharedRest.js @@ -6,12 +6,16 @@ const classesWithMasterOnlyAccess = [ '_JobSchedule', '_Idempotency', ]; +const { createSanitizedError } = require('./Error'); + // Disallowing access to the _Role collection except by master key function enforceRoleSecurity(method, className, auth) { if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) { if (method === 'delete' || method === 'find') { - const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + `Clients aren't allowed to perform the ${method} operation on the installation collection.` + ); } } @@ -21,14 +25,18 @@ function enforceRoleSecurity(method, className, auth) { !auth.isMaster && !auth.isMaintenance ) { - const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + `Clients aren't allowed to perform the ${method} operation on the ${className} collection.` + ); } // readOnly masterKey is not allowed if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) { - const error = `read-only masterKey isn't allowed to perform the ${method} operation.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + `read-only masterKey isn't allowed to perform the ${method} operation.` + ); } } diff --git a/src/TestUtils.js b/src/TestUtils.js index 2cd1493511..ec4cb29554 100644 --- a/src/TestUtils.js +++ b/src/TestUtils.js @@ -81,3 +81,4 @@ export class Connections { return this.sockets.size; } } + diff --git a/src/middlewares.js b/src/middlewares.js index 8ba3a9acff..2da7016b4f 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -13,6 +13,7 @@ import { pathToRegexp } from 'path-to-regexp'; import RedisStore from 'rate-limit-redis'; import { createClient } from 'redis'; import { BlockList, isIPv4 } from 'net'; +import { createSanitizedHttpError } from './Error'; export const DEFAULT_ALLOWED_HEADERS = 'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, X-Parse-Request-Id, Content-Type, Pragma, Cache-Control'; @@ -501,8 +502,9 @@ export function handleParseErrors(err, req, res, next) { export function enforceMasterKeyAccess(req, res, next) { if (!req.auth.isMaster) { - res.status(403); - res.end('{"error":"unauthorized: master key is required"}'); + const error = createSanitizedHttpError(403, 'unauthorized: master key is required'); + res.status(error.status); + res.end(`{"error":"${error.message}"}`); return; } next(); @@ -510,10 +512,7 @@ export function enforceMasterKeyAccess(req, res, next) { export function promiseEnforceMasterKeyAccess(request) { if (!request.auth.isMaster) { - const error = new Error(); - error.status = 403; - error.message = 'unauthorized: master key is required'; - throw error; + throw createSanitizedHttpError(403, 'unauthorized: master key is required'); } return Promise.resolve(); } diff --git a/src/rest.js b/src/rest.js index e2e688a972..66feae66f0 100644 --- a/src/rest.js +++ b/src/rest.js @@ -13,6 +13,7 @@ var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); var triggers = require('./triggers'); const { enforceRoleSecurity } = require('./SharedRest'); +const { createSanitizedError } = require('./Error'); function checkTriggers(className, config, types) { return types.some(triggerType => { @@ -195,7 +196,7 @@ function del(config, auth, className, objectId, context) { firstResult.className = className; if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) { if (!auth.user || firstResult.user.objectId !== auth.user.id) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } } var cacheAdapter = config.cacheController; @@ -326,7 +327,7 @@ function handleSessionMissingError(error, className, auth) { !auth.isMaster && !auth.isMaintenance ) { - throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth.'); + throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.'); } throw error; } From 0ff97793cd5501a97e34d659eb015dc58dcc6089 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 23 Nov 2025 12:52:31 +0000 Subject: [PATCH 39/50] chore(release): 8.5.0-alpha.13 [skip ci] # [8.5.0-alpha.13](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.12...8.5.0-alpha.13) (2025-11-23) ### Bug Fixes * Server internal error details leaking in error messages returned to clients ([#9937](https://github.com/parse-community/parse-server/issues/9937)) ([50edb5a](https://github.com/parse-community/parse-server/commit/50edb5ab4bb4a6ce474bfb7cf159d918933753b8)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index fb6d1640cf..c7f7d32a92 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.13](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.12...8.5.0-alpha.13) (2025-11-23) + + +### Bug Fixes + +* Server internal error details leaking in error messages returned to clients ([#9937](https://github.com/parse-community/parse-server/issues/9937)) ([50edb5a](https://github.com/parse-community/parse-server/commit/50edb5ab4bb4a6ce474bfb7cf159d918933753b8)) + # [8.5.0-alpha.12](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.11...8.5.0-alpha.12) (2025-11-19) diff --git a/package-lock.json b/package-lock.json index c1d21724e0..4b7d47e111 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.12", + "version": "8.5.0-alpha.13", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.12", + "version": "8.5.0-alpha.13", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 261700884a..ba56857991 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.12", + "version": "8.5.0-alpha.13", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 12beb8f6ee5d3002fec017bb4525eb3f1375f806 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 23 Nov 2025 14:09:25 +0100 Subject: [PATCH 40/50] fix: Parse Server option `rateLimit.zone` does not use default value `ip` (#9941) --- src/Options/Definitions.js | 3 ++- src/Options/docs.js | 2 +- src/Options/index.js | 19 +++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 1ae9512823..6eeff0ed57 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -690,7 +690,8 @@ module.exports.RateLimitOptions = { zone: { env: 'PARSE_SERVER_RATE_LIMIT_ZONE', help: - "The type of rate limit to apply. The following types are supported:

- `global`: rate limit based on the number of requests made by all users
- `ip`: rate limit based on the IP address of the request
- `user`: rate limit based on the user ID of the request
- `session`: rate limit based on the session token of the request


:default: 'ip'", + 'The type of rate limit to apply. The following types are supported:
  • `global`: rate limit based on the number of requests made by all users
  • `ip`: rate limit based on the IP address of the request
  • `user`: rate limit based on the user ID of the request
  • `session`: rate limit based on the session token of the request
Default is `ip`.', + default: 'ip', }, }; module.exports.SecurityOptions = { diff --git a/src/Options/docs.js b/src/Options/docs.js index cdbd06de45..03fa9cc981 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -122,7 +122,7 @@ * @property {String[]} requestMethods Optional, the HTTP request methods to which the rate limit should be applied, default is all methods. * @property {String} requestPath The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expression. See: https://expressjs.com/en/guide/routing.html * @property {Number} requestTimeWindow The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied. - * @property {String} zone The type of rate limit to apply. The following types are supported:

- `global`: rate limit based on the number of requests made by all users
- `ip`: rate limit based on the IP address of the request
- `user`: rate limit based on the user ID of the request
- `session`: rate limit based on the session token of the request


:default: 'ip' + * @property {String} zone The type of rate limit to apply. The following types are supported:
  • `global`: rate limit based on the number of requests made by all users
  • `ip`: rate limit based on the IP address of the request
  • `user`: rate limit based on the user ID of the request
  • `session`: rate limit based on the session token of the request
Default is `ip`. */ /** diff --git a/src/Options/index.js b/src/Options/index.js index 81dbc3c536..11ab00a00f 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -370,16 +370,15 @@ export interface RateLimitOptions { /* Optional, the URL of the Redis server to store rate limit data. This allows to rate limit requests for multiple servers by calculating the sum of all requests across all servers. This is useful if multiple servers are processing requests behind a load balancer. For example, the limit of 10 requests is reached if each of 2 servers processed 5 requests. */ redisUrl: ?string; - /* - The type of rate limit to apply. The following types are supported: -

- - `global`: rate limit based on the number of requests made by all users
- - `ip`: rate limit based on the IP address of the request
- - `user`: rate limit based on the user ID of the request
- - `session`: rate limit based on the session token of the request
-

- :default: 'ip' - */ + /* The type of rate limit to apply. The following types are supported: +
    +
  • `global`: rate limit based on the number of requests made by all users
  • +
  • `ip`: rate limit based on the IP address of the request
  • +
  • `user`: rate limit based on the user ID of the request
  • +
  • `session`: rate limit based on the session token of the request
  • +
+ Default is `ip`. + :DEFAULT: ip */ zone: ?string; } From 8aae732ace2435d138f8f6a5837961d0f732f856 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 23 Nov 2025 13:10:13 +0000 Subject: [PATCH 41/50] chore(release): 8.5.0-alpha.14 [skip ci] # [8.5.0-alpha.14](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.13...8.5.0-alpha.14) (2025-11-23) ### Bug Fixes * Parse Server option `rateLimit.zone` does not use default value `ip` ([#9941](https://github.com/parse-community/parse-server/issues/9941)) ([12beb8f](https://github.com/parse-community/parse-server/commit/12beb8f6ee5d3002fec017bb4525eb3f1375f806)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index c7f7d32a92..d7c1b5d500 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.14](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.13...8.5.0-alpha.14) (2025-11-23) + + +### Bug Fixes + +* Parse Server option `rateLimit.zone` does not use default value `ip` ([#9941](https://github.com/parse-community/parse-server/issues/9941)) ([12beb8f](https://github.com/parse-community/parse-server/commit/12beb8f6ee5d3002fec017bb4525eb3f1375f806)) + # [8.5.0-alpha.13](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.12...8.5.0-alpha.13) (2025-11-23) diff --git a/package-lock.json b/package-lock.json index 4b7d47e111..305e978525 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.13", + "version": "8.5.0-alpha.14", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.13", + "version": "8.5.0-alpha.14", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index ba56857991..139c75ddef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.13", + "version": "8.5.0-alpha.14", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From d4c6de0096b3ac95289c6bddfe25eb397d790e41 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Sun, 23 Nov 2025 18:10:44 +0100 Subject: [PATCH 42/50] perf: Remove unused dependencies (#9943) --- package-lock.json | 463 +++++++++++++++++++++++++++++++++------------- package.json | 6 +- 2 files changed, 337 insertions(+), 132 deletions(-) diff --git a/package-lock.json b/package-lock.json index 305e978525..5997b2e588 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "license": "Apache-2.0", "dependencies": { "@apollo/server": "4.12.1", - "@babel/eslint-parser": "7.28.0", "@graphql-tools/merge": "9.0.24", "@graphql-tools/schema": "10.0.23", "@graphql-tools/utils": "10.8.6", @@ -27,7 +26,6 @@ "graphql": "16.11.0", "graphql-list-fields": "2.0.4", "graphql-relay": "0.10.2", - "graphql-tag": "2.12.6", "graphql-upload": "15.0.2", "intersect": "1.0.1", "jsonwebtoken": "9.0.2", @@ -47,7 +45,6 @@ "punycode": "2.3.1", "rate-limit-redis": "4.2.0", "redis": "4.7.0", - "router": "2.2.0", "semver": "7.7.2", "subscriptions-transport-ws": "0.11.0", "tv4": "1.3.0", @@ -64,6 +61,7 @@ "@apollo/client": "3.13.8", "@babel/cli": "7.27.0", "@babel/core": "7.27.4", + "@babel/eslint-parser": "7.28.0", "@babel/plugin-proposal-object-rest-spread": "7.20.7", "@babel/plugin-transform-flow-strip-types": "7.26.5", "@babel/preset-env": "7.27.2", @@ -83,11 +81,9 @@ "eslint": "9.27.0", "eslint-plugin-expect-type": "0.6.2", "eslint-plugin-unused-imports": "4.3.0", - "flow-bin": "0.271.0", "form-data": "4.0.4", "globals": "16.2.0", "graphql-tag": "2.12.6", - "husky": "9.1.7", "jasmine": "5.7.1", "jasmine-spec-reporter": "7.0.0", "jsdoc": "4.0.4", @@ -156,6 +152,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -838,6 +835,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -852,6 +850,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -861,6 +860,7 @@ "version": "7.27.4", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -890,12 +890,14 @@ "node_modules/@babel/core/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -904,6 +906,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz", "integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==", + "dev": true, "license": "MIT", "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", @@ -922,6 +925,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -930,6 +934,7 @@ "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.27.3", @@ -946,6 +951,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -972,6 +978,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.27.2", @@ -988,6 +995,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "dependencies": { "yallist": "^3.0.2" } @@ -996,6 +1004,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -1003,7 +1012,8 @@ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.27.1", @@ -1098,6 +1108,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -1111,6 +1122,7 @@ "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", @@ -1201,6 +1213,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1210,6 +1223,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1219,6 +1233,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1243,6 +1258,7 @@ "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", @@ -1256,6 +1272,7 @@ "version": "7.27.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -2518,6 +2535,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -2532,6 +2550,7 @@ "version": "7.27.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -2550,6 +2569,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, "engines": { "node": ">=4" } @@ -2558,6 +2578,7 @@ "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -2632,6 +2653,7 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -2650,6 +2672,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -2661,6 +2684,7 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -2670,6 +2694,7 @@ "version": "0.20.0", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", @@ -2684,6 +2709,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2693,6 +2719,7 @@ "version": "0.14.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -2705,6 +2732,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -2728,6 +2756,7 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2740,6 +2769,7 @@ "version": "9.27.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2752,6 +2782,7 @@ "version": "2.1.6", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2761,6 +2792,7 @@ "version": "0.3.4", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" @@ -2773,6 +2805,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -3190,6 +3223,7 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -3199,6 +3233,7 @@ "version": "0.16.6", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", @@ -3212,6 +3247,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -3225,6 +3261,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, "engines": { "node": ">=12.22" }, @@ -3237,6 +3274,7 @@ "version": "0.4.2", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -3454,6 +3492,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -3466,6 +3505,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -3474,6 +3514,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -3505,12 +3546,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -3707,6 +3750,7 @@ "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, "dependencies": { "eslint-scope": "5.1.1" } @@ -6322,6 +6366,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, "license": "MIT" }, "node_modules/@types/express": { @@ -6362,6 +6407,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, "license": "MIT" }, "node_modules/@types/jsonwebtoken": { @@ -7429,6 +7475,7 @@ "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -7441,6 +7488,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -7653,7 +7701,8 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/argv-formatter": { "version": "1.0.0", @@ -7849,7 +7898,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -7992,6 +8042,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8013,6 +8064,7 @@ "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -8265,6 +8317,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "engines": { "node": ">=6" } @@ -8292,6 +8345,7 @@ "version": "1.0.30001707", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -8896,7 +8950,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/config-chain": { "version": "1.1.13", @@ -9095,6 +9150,7 @@ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.1" }, @@ -9123,6 +9179,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -9447,7 +9504,8 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, "node_modules/deepcopy": { "version": "2.1.0", @@ -9824,6 +9882,7 @@ "version": "1.5.129", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz", "integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==", + "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { @@ -10120,6 +10179,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "devOptional": true, "engines": { "node": ">=6" } @@ -10172,6 +10232,7 @@ "version": "9.27.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -10267,6 +10328,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -10279,6 +10341,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, "engines": { "node": ">=10" } @@ -10287,6 +10350,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -10301,6 +10365,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10316,6 +10381,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -10326,12 +10392,14 @@ "node_modules/eslint/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "engines": { "node": ">=10" }, @@ -10343,6 +10411,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -10359,6 +10428,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10371,6 +10441,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -10380,6 +10451,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -10391,6 +10463,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -10399,6 +10472,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10410,6 +10484,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.14.0", @@ -10427,6 +10502,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10452,6 +10528,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -10463,6 +10540,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "engines": { "node": ">=4.0" } @@ -10471,6 +10549,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -10482,6 +10561,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "engines": { "node": ">=4.0" } @@ -10490,6 +10570,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, "engines": { "node": ">=4.0" } @@ -10504,6 +10585,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -10781,7 +10863,8 @@ "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "node_modules/fast-xml-parser": { "version": "4.5.3", @@ -10911,6 +10994,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" @@ -11123,6 +11207,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -11191,6 +11276,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -11204,20 +11290,8 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "license": "ISC" - }, - "node_modules/flow-bin": { - "version": "0.271.0", - "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.271.0.tgz", - "integrity": "sha512-BQjk0DenuPLbB/WlpQzDkSnObOPdzR+PBDItZlawApH/56fqYlM40WuBLs+cfUjjaByML46WHyOAWlQoWnPnjQ==", "dev": true, - "license": "MIT", - "bin": { - "flow": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } + "license": "ISC" }, "node_modules/fn.name": { "version": "1.1.0", @@ -11686,6 +11760,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -12591,21 +12666,6 @@ "node": ">=10.17.0" } }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true, - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -12647,6 +12707,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "engines": { "node": ">= 4" } @@ -12655,6 +12716,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -12693,6 +12755,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "engines": { "node": ">=0.8.19" } @@ -12824,6 +12887,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -12840,6 +12904,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -12992,7 +13057,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/isstream": { "version": "0.1.2", @@ -13327,12 +13393,14 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -13418,6 +13486,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -13436,7 +13505,8 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, "node_modules/json-parse-better-errors": { "version": "1.0.2", @@ -13463,7 +13533,8 @@ "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true }, "node_modules/json-stringify-safe": { "version": "5.0.1", @@ -13474,6 +13545,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -13611,6 +13683,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -13663,6 +13736,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -13931,6 +14005,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -14021,7 +14096,8 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, "node_modules/lodash.once": { "version": "4.1.1", @@ -14842,6 +14918,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -15251,7 +15328,8 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "node_modules/negotiator": { "version": "0.6.3", @@ -15378,6 +15456,7 @@ "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, "license": "MIT" }, "node_modules/node-source-walk": { @@ -18341,6 +18420,7 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -18520,6 +18600,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -18534,6 +18615,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -18597,6 +18679,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -18736,6 +18819,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "engines": { "node": ">=8" } @@ -18753,6 +18837,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -19255,6 +19340,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "engines": { "node": ">= 0.8.0" } @@ -19950,6 +20036,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "engines": { "node": ">=4" } @@ -20734,6 +20821,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -20745,6 +20833,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -21328,6 +21417,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "engines": { "node": ">=8" }, @@ -22008,6 +22098,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -22278,6 +22369,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -22561,6 +22653,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -22694,6 +22787,7 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -22997,6 +23091,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, "engines": { "node": ">=10" }, @@ -23079,6 +23174,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -23575,6 +23671,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", @@ -23584,12 +23681,14 @@ "@babel/compat-data": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", - "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==" + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "dev": true }, "@babel/core": { "version": "7.27.4", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -23611,12 +23710,14 @@ "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, @@ -23624,6 +23725,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz", "integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==", + "dev": true, "requires": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", @@ -23633,7 +23735,8 @@ "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, @@ -23641,6 +23744,7 @@ "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", + "dev": true, "requires": { "@babel/parser": "^7.27.3", "@babel/types": "^7.27.3", @@ -23653,6 +23757,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, "requires": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -23674,6 +23779,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, "requires": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", @@ -23686,6 +23792,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "requires": { "yallist": "^3.0.2" } @@ -23693,12 +23800,14 @@ "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, @@ -23771,6 +23880,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, "requires": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" @@ -23780,6 +23890,7 @@ "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, "requires": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", @@ -23836,17 +23947,20 @@ "@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true }, "@babel/helper-validator-identifier": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true }, "@babel/helper-validator-option": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==" + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true }, "@babel/helper-wrap-function": { "version": "7.27.1", @@ -23863,6 +23977,7 @@ "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, "requires": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" @@ -23872,6 +23987,7 @@ "version": "7.27.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "dev": true, "requires": { "@babel/types": "^7.27.3" } @@ -24648,6 +24764,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, "requires": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", @@ -24658,6 +24775,7 @@ "version": "7.27.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, "requires": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", @@ -24671,7 +24789,8 @@ "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true } } }, @@ -24679,6 +24798,7 @@ "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "dev": true, "requires": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" @@ -24743,6 +24863,7 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, "requires": { "eslint-visitor-keys": "^3.4.3" }, @@ -24750,19 +24871,22 @@ "eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true } } }, "@eslint-community/regexpp": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==" + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true }, "@eslint/config-array": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, "requires": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -24772,12 +24896,14 @@ "@eslint/config-helpers": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==" + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true }, "@eslint/core": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, "requires": { "@types/json-schema": "^7.0.15" } @@ -24786,6 +24912,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -24801,24 +24928,28 @@ "globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true } } }, "@eslint/js": { "version": "9.27.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==" + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "dev": true }, "@eslint/object-schema": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==" + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true }, "@eslint/plugin-kit": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, "requires": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" @@ -24828,6 +24959,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, "requires": { "@types/json-schema": "^7.0.15" } @@ -25124,12 +25256,14 @@ "@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==" + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true }, "@humanfs/node": { "version": "0.16.6", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, "requires": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" @@ -25138,19 +25272,22 @@ "@humanwhocodes/retry": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==" + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true } } }, "@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true }, "@humanwhocodes/retry": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==" + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true }, "@isaacs/cliui": { "version": "8.0.2", @@ -25304,6 +25441,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, "requires": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -25312,12 +25450,14 @@ "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true }, "@jridgewell/set-array": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true }, "@jridgewell/source-map": { "version": "0.3.6", @@ -25345,12 +25485,14 @@ "@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true }, "@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -25526,6 +25668,7 @@ "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, "requires": { "eslint-scope": "5.1.1" } @@ -27319,7 +27462,8 @@ "@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true }, "@types/express": { "version": "4.17.21", @@ -27357,7 +27501,8 @@ "@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, "@types/jsonwebtoken": { "version": "9.0.5", @@ -28083,12 +28228,14 @@ "acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==" + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "requires": {} }, "agent-base": { @@ -28244,7 +28391,8 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "argv-formatter": { "version": "1.0.0", @@ -28400,7 +28548,8 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "base64-js": { "version": "1.5.1", @@ -28509,6 +28658,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -28527,6 +28677,7 @@ "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, "requires": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -28689,7 +28840,8 @@ "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true }, "camel-case": { "version": "4.1.2", @@ -28710,7 +28862,8 @@ "caniuse-lite": { "version": "1.0.30001707", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", - "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==" + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "dev": true }, "cardinal": { "version": "2.1.1", @@ -29139,7 +29292,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "config-chain": { "version": "1.1.13", @@ -29290,6 +29444,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -29521,7 +29676,8 @@ "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, "deepcopy": { "version": "2.1.0", @@ -29806,7 +29962,8 @@ "electron-to-chromium": { "version": "1.5.129", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz", - "integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==" + "integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==", + "dev": true }, "emoji-regex": { "version": "8.0.0", @@ -30008,7 +30165,8 @@ "escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "devOptional": true }, "escape-html": { "version": "1.0.3", @@ -30045,6 +30203,7 @@ "version": "9.27.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -30087,6 +30246,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -30095,6 +30255,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -30104,6 +30265,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -30111,17 +30273,20 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true }, "eslint-scope": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -30130,17 +30295,20 @@ "eslint-visitor-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==" + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true }, "estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "requires": { "is-glob": "^4.0.3" } @@ -30148,12 +30316,14 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -30182,6 +30352,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -30190,12 +30361,14 @@ "eslint-visitor-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==" + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true }, "espree": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, "requires": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", @@ -30205,7 +30378,8 @@ "eslint-visitor-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==" + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true } } }, @@ -30219,6 +30393,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, "requires": { "estraverse": "^5.1.0" }, @@ -30226,7 +30401,8 @@ "estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true } } }, @@ -30234,6 +30410,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "requires": { "estraverse": "^5.2.0" }, @@ -30241,14 +30418,16 @@ "estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true } } }, "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true }, "estree-walker": { "version": "2.0.2", @@ -30259,7 +30438,8 @@ "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true }, "etag": { "version": "1.8.1", @@ -30452,7 +30632,8 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "fast-xml-parser": { "version": "4.5.3", @@ -30538,6 +30719,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, "requires": { "flat-cache": "^4.0.0" } @@ -30689,6 +30871,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "requires": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -30733,6 +30916,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, "requires": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -30741,12 +30925,7 @@ "flatted": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==" - }, - "flow-bin": { - "version": "0.271.0", - "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.271.0.tgz", - "integrity": "sha512-BQjk0DenuPLbB/WlpQzDkSnObOPdzR+PBDItZlawApH/56fqYlM40WuBLs+cfUjjaByML46WHyOAWlQoWnPnjQ==", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "dev": true }, "fn.name": { @@ -31065,7 +31244,8 @@ "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true }, "get-amd-module-type": { "version": "6.0.0", @@ -31711,12 +31891,6 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, - "husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true - }, "iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -31739,12 +31913,14 @@ "ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -31769,7 +31945,8 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true }, "indent-string": { "version": "4.0.0", @@ -31864,7 +32041,8 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -31875,6 +32053,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -31975,7 +32154,8 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "isstream": { "version": "0.1.2", @@ -32228,12 +32408,14 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, "requires": { "argparse": "^2.0.1" } @@ -32302,7 +32484,8 @@ "jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==" + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true }, "json-bigint": { "version": "1.0.0", @@ -32315,7 +32498,8 @@ "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, "json-parse-better-errors": { "version": "1.0.2", @@ -32342,7 +32526,8 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true }, "json-stringify-safe": { "version": "5.0.1", @@ -32352,7 +32537,8 @@ "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true }, "jsonfile": { "version": "6.1.0", @@ -32461,6 +32647,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "requires": { "json-buffer": "3.0.1" } @@ -32513,6 +32700,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "requires": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -32696,6 +32884,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "requires": { "p-locate": "^5.0.0" } @@ -32779,7 +32968,8 @@ "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, "lodash.once": { "version": "4.1.1", @@ -33329,6 +33519,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -33595,7 +33786,8 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "negotiator": { "version": "0.6.3", @@ -33683,7 +33875,8 @@ "node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true }, "node-source-walk": { "version": "7.0.0", @@ -35691,6 +35884,7 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "requires": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -35815,6 +36009,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, "requires": { "yocto-queue": "^0.1.0" } @@ -35823,6 +36018,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "requires": { "p-limit": "^3.0.2" } @@ -35871,6 +36067,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "requires": { "callsites": "^3.0.0" } @@ -35965,7 +36162,8 @@ "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true }, "path-is-absolute": { "version": "1.0.1", @@ -35976,7 +36174,8 @@ "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true }, "path-parse": { "version": "1.0.7", @@ -36317,7 +36516,8 @@ "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true }, "prettier": { "version": "2.0.5", @@ -36833,7 +37033,8 @@ "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true }, "resolve-pkg-maps": { "version": "1.0.0", @@ -37352,6 +37553,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -37359,7 +37561,8 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true }, "showdown": { "version": "2.1.0", @@ -37804,7 +38007,8 @@ "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true }, "strnum": { "version": "1.1.2", @@ -38281,6 +38485,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "requires": { "prelude-ls": "^1.2.1" } @@ -38461,6 +38666,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, "requires": { "escalade": "^3.2.0", "picocolors": "^1.1.0" @@ -38666,6 +38872,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "requires": { "isexe": "^2.0.0" } @@ -38769,7 +38976,8 @@ "word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true }, "wordwrap": { "version": "1.0.0", @@ -38994,7 +39202,8 @@ "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true }, "yoctocolors": { "version": "2.1.1", diff --git a/package.json b/package.json index 139c75ddef..bb7f9af433 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "license": "Apache-2.0", "dependencies": { "@apollo/server": "4.12.1", - "@babel/eslint-parser": "7.28.0", "@graphql-tools/merge": "9.0.24", "@graphql-tools/schema": "10.0.23", "@graphql-tools/utils": "10.8.6", @@ -37,7 +36,6 @@ "graphql": "16.11.0", "graphql-list-fields": "2.0.4", "graphql-relay": "0.10.2", - "graphql-tag": "2.12.6", "graphql-upload": "15.0.2", "intersect": "1.0.1", "jsonwebtoken": "9.0.2", @@ -57,7 +55,6 @@ "punycode": "2.3.1", "rate-limit-redis": "4.2.0", "redis": "4.7.0", - "router": "2.2.0", "semver": "7.7.2", "subscriptions-transport-ws": "0.11.0", "tv4": "1.3.0", @@ -67,6 +64,7 @@ "ws": "8.18.2" }, "devDependencies": { + "@babel/eslint-parser": "7.28.0", "@actions/core": "1.11.1", "@apollo/client": "3.13.8", "@babel/cli": "7.27.0", @@ -90,11 +88,9 @@ "eslint": "9.27.0", "eslint-plugin-expect-type": "0.6.2", "eslint-plugin-unused-imports": "4.3.0", - "flow-bin": "0.271.0", "form-data": "4.0.4", "globals": "16.2.0", "graphql-tag": "2.12.6", - "husky": "9.1.7", "jasmine": "5.7.1", "jasmine-spec-reporter": "7.0.0", "jsdoc": "4.0.4", From 73e78127c2c0f9747b133f83c6bcf701845fe23c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 23 Nov 2025 17:11:36 +0000 Subject: [PATCH 43/50] chore(release): 8.5.0-alpha.15 [skip ci] # [8.5.0-alpha.15](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.14...8.5.0-alpha.15) (2025-11-23) ### Performance Improvements * Remove unused dependencies ([#9943](https://github.com/parse-community/parse-server/issues/9943)) ([d4c6de0](https://github.com/parse-community/parse-server/commit/d4c6de0096b3ac95289c6bddfe25eb397d790e41)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index d7c1b5d500..d0dd432adc 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.15](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.14...8.5.0-alpha.15) (2025-11-23) + + +### Performance Improvements + +* Remove unused dependencies ([#9943](https://github.com/parse-community/parse-server/issues/9943)) ([d4c6de0](https://github.com/parse-community/parse-server/commit/d4c6de0096b3ac95289c6bddfe25eb397d790e41)) + # [8.5.0-alpha.14](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.13...8.5.0-alpha.14) (2025-11-23) diff --git a/package-lock.json b/package-lock.json index 5997b2e588..ee7f2cd789 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.14", + "version": "8.5.0-alpha.15", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.14", + "version": "8.5.0-alpha.15", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index bb7f9af433..45a8bf12b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.14", + "version": "8.5.0-alpha.15", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 47521974aeafcf41102be62f19612a4ab0a4837f Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Fri, 28 Nov 2025 19:48:35 +0100 Subject: [PATCH 44/50] feat: Add Parse Server option `enableSanitizedErrorResponse` to remove detailed error messages from responses sent to clients (#9944) --- spec/ParseFile.spec.js | 4 +-- spec/Utils.spec.js | 41 +++++++++++++++++++++++++- src/Controllers/SchemaController.js | 13 +++++--- src/Error.js | 8 ++--- src/GraphQL/loaders/schemaMutations.js | 11 ++++--- src/GraphQL/loaders/schemaQueries.js | 4 +-- src/GraphQL/loaders/usersQueries.js | 4 +-- src/GraphQL/parseGraphQLUtils.js | 3 +- src/Options/Definitions.js | 7 +++++ src/Options/docs.js | 1 + src/Options/index.js | 3 ++ src/RestQuery.js | 10 ++++--- src/RestWrite.js | 8 +++-- src/Routers/ClassesRouter.js | 2 +- src/Routers/FilesRouter.js | 4 +-- src/Routers/GlobalConfigRouter.js | 1 + src/Routers/GraphQLRouter.js | 1 + src/Routers/PurgeRouter.js | 1 + src/Routers/PushRouter.js | 1 + src/Routers/SchemasRouter.js | 3 ++ src/Routers/UsersRouter.js | 5 ++-- src/SharedRest.js | 11 ++++--- src/middlewares.js | 4 +-- src/rest.js | 20 ++++++------- 24 files changed, 121 insertions(+), 49 deletions(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 7e6f5e5080..5c1c3c99e7 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -767,13 +767,11 @@ describe('Parse.File testing', () => { describe('getting files', () => { it('does not crash on file request with invalid app ID', async () => { - loggerErrorSpy.calls.reset(); const res1 = await request({ url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt', }).catch(e => e); expect(res1.status).toBe(403); - expect(res1.data).toEqual({ code: 119, error: 'Permission denied' }); - expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid application ID.')); + expect(res1.data).toEqual({ code: 119, error: 'Invalid application ID.' }); // Ensure server did not crash const res2 = await request({ url: 'http://localhost:8378/1/health' }); expect(res2.status).toEqual(200); diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js index 2bbc5656a2..a473064376 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -1,4 +1,5 @@ -const Utils = require('../src/Utils'); +const Utils = require('../lib/Utils'); +const { createSanitizedError, createSanitizedHttpError } = require("../lib/Error") describe('Utils', () => { describe('encodeForUrl', () => { @@ -173,4 +174,42 @@ describe('Utils', () => { expect(Utils.getNestedProperty(obj, 'database.name')).toBe(''); }); }); + + describe('createSanitizedError', () => { + it('should return "Permission denied" when enableSanitizedErrorResponse is true', () => { + const config = { enableSanitizedErrorResponse: true }; + const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config); + expect(error.message).toBe('Permission denied'); + }); + + it('should not crash with config undefined', () => { + const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', undefined); + expect(error.message).toBe('Permission denied'); + }); + + it('should return the detailed message when enableSanitizedErrorResponse is false', () => { + const config = { enableSanitizedErrorResponse: false }; + const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config); + expect(error.message).toBe('Detailed error message'); + }); + }); + + describe('createSanitizedHttpError', () => { + it('should return "Permission denied" when enableSanitizedErrorResponse is true', () => { + const config = { enableSanitizedErrorResponse: true }; + const error = createSanitizedHttpError(403, 'Detailed error message', config); + expect(error.message).toBe('Permission denied'); + }); + + it('should not crash with config undefined', () => { + const error = createSanitizedHttpError(403, 'Detailed error message', undefined); + expect(error.message).toBe('Permission denied'); + }); + + it('should return the detailed message when enableSanitizedErrorResponse is false', () => { + const config = { enableSanitizedErrorResponse: false }; + const error = createSanitizedHttpError(403, 'Detailed error message', config); + expect(error.message).toBe('Detailed error message'); + }); + }); }); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 70694925bf..b605fba632 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1399,6 +1399,7 @@ export default class SchemaController { return true; } const perms = classPermissions[operation]; + const config = Config.get(Parse.applicationId) // If only for authenticated users // make sure we have an aclGroup if (perms['requiresAuthentication']) { @@ -1406,12 +1407,14 @@ export default class SchemaController { if (!aclGroup || aclGroup.length == 0) { throw createSanitizedError( Parse.Error.OBJECT_NOT_FOUND, - 'Permission denied, user needs to be authenticated.' + 'Permission denied, user needs to be authenticated.', + config ); } else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) { throw createSanitizedError( Parse.Error.OBJECT_NOT_FOUND, - 'Permission denied, user needs to be authenticated.' + 'Permission denied, user needs to be authenticated.', + config ); } // requiresAuthentication passed, just move forward @@ -1428,7 +1431,8 @@ export default class SchemaController { if (permissionField == 'writeUserFields' && operation == 'create') { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - `Permission denied for action ${operation} on class ${className}.` + `Permission denied for action ${operation} on class ${className}.`, + config ); } @@ -1451,7 +1455,8 @@ export default class SchemaController { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - `Permission denied for action ${operation} on class ${className}.` + `Permission denied for action ${operation} on class ${className}.`, + config ); } diff --git a/src/Error.js b/src/Error.js index 4729965e1c..75eff3d673 100644 --- a/src/Error.js +++ b/src/Error.js @@ -8,7 +8,7 @@ import defaultLogger from './logger'; * @param {string} detailedMessage - The detailed error message to log server-side * @returns {Parse.Error} A Parse.Error with sanitized message */ -function createSanitizedError(errorCode, detailedMessage) { +function createSanitizedError(errorCode, detailedMessage, config) { // On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file if (process.env.TESTING) { defaultLogger.error('Sanitized error:', detailedMessage); @@ -16,7 +16,7 @@ function createSanitizedError(errorCode, detailedMessage) { defaultLogger.error(detailedMessage); } - return new Parse.Error(errorCode, 'Permission denied'); + return new Parse.Error(errorCode, config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage); } /** @@ -27,7 +27,7 @@ function createSanitizedError(errorCode, detailedMessage) { * @param {string} detailedMessage - The detailed error message to log server-side * @returns {Error} An Error with sanitized message */ -function createSanitizedHttpError(statusCode, detailedMessage) { +function createSanitizedHttpError(statusCode, detailedMessage, config) { // On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file if (process.env.TESTING) { defaultLogger.error('Sanitized error:', detailedMessage); @@ -37,7 +37,7 @@ function createSanitizedHttpError(statusCode, detailedMessage) { const error = new Error(); error.status = statusCode; - error.message = 'Permission denied'; + error.message = config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage; return error; } diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index 5dd8969bd9..93cd89d54a 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -31,12 +31,13 @@ const load = parseGraphQLSchema => { const { name, schemaFields } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); if (auth.isReadOnly) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to create a schema.", + config ); } @@ -80,12 +81,13 @@ const load = parseGraphQLSchema => { const { name, schemaFields } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); if (auth.isReadOnly) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to update a schema." + "read-only masterKey isn't allowed to update a schema.", + config ); } @@ -131,12 +133,13 @@ const load = parseGraphQLSchema => { const { name } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); if (auth.isReadOnly) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to delete a schema.", + config ); } diff --git a/src/GraphQL/loaders/schemaQueries.js b/src/GraphQL/loaders/schemaQueries.js index 25bc071919..2956b47934 100644 --- a/src/GraphQL/loaders/schemaQueries.js +++ b/src/GraphQL/loaders/schemaQueries.js @@ -31,7 +31,7 @@ const load = parseGraphQLSchema => { const { name } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); const schema = await config.database.loadSchema({ clearCache: true }); const parseClass = await getClass(name, schema); @@ -57,7 +57,7 @@ const load = parseGraphQLSchema => { try { const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); const schema = await config.database.loadSchema({ clearCache: true }); return (await schema.getAllClasses(true)).map(parseClass => ({ diff --git a/src/GraphQL/loaders/usersQueries.js b/src/GraphQL/loaders/usersQueries.js index a51e9553c0..dc9f57f5ef 100644 --- a/src/GraphQL/loaders/usersQueries.js +++ b/src/GraphQL/loaders/usersQueries.js @@ -9,7 +9,7 @@ import { createSanitizedError } from '../../Error'; const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) => { const { info, config } = context; if (!info || !info.sessionToken) { - throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config); } const sessionToken = info.sessionToken; const selectedFields = getFieldNames(queryInfo) @@ -63,7 +63,7 @@ const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) = info.context ); if (!response.results || response.results.length == 0) { - throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config); } else { const user = response.results[0]; return { diff --git a/src/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js index 1a0b266a36..ba5fd1b416 100644 --- a/src/GraphQL/parseGraphQLUtils.js +++ b/src/GraphQL/parseGraphQLUtils.js @@ -2,11 +2,12 @@ import Parse from 'parse/node'; import { GraphQLError } from 'graphql'; import { createSanitizedError } from '../Error'; -export function enforceMasterKeyAccess(auth) { +export function enforceMasterKeyAccess(auth, config) { if (!auth.isMaster) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, 'unauthorized: master key is required', + config ); } } diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 6eeff0ed57..774b4505e1 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -247,6 +247,13 @@ module.exports.ParseServerOptions = { action: parsers.booleanParser, default: true, }, + enableSanitizedErrorResponse: { + env: 'PARSE_SERVER_ENABLE_SANITIZED_ERROR_RESPONSE', + help: + 'If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`.', + action: parsers.booleanParser, + default: true, + }, encodeParseObjectInCloudFunction: { env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index 03fa9cc981..9569239ef7 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -45,6 +45,7 @@ * @property {Boolean} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`. * @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors * @property {Boolean} enableInsecureAuthAdapters Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them. + * @property {Boolean} enableSanitizedErrorResponse If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`. * @property {Boolean} encodeParseObjectInCloudFunction If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`.

ℹ️ The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`. * @property {String} encryptionKey Key for encrypting your files * @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access. diff --git a/src/Options/index.js b/src/Options/index.js index 11ab00a00f..cdeb7cd846 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -347,6 +347,9 @@ export interface ParseServerOptions { rateLimit: ?(RateLimitOptions[]); /* Options to customize the request context using inversion of control/dependency injection.*/ requestContextMiddleware: ?(req: any, res: any, next: any) => void; + /* If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`. + :DEFAULT: true */ + enableSanitizedErrorResponse: ?boolean; } export interface RateLimitOptions { diff --git a/src/RestQuery.js b/src/RestQuery.js index b102caea23..2064ffd0df 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -52,7 +52,7 @@ async function RestQuery({ throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad query type'); } const isGet = method === RestQuery.Method.get; - enforceRoleSecurity(method, className, auth); + enforceRoleSecurity(method, className, auth, config); const result = runBeforeFind ? await triggers.maybeRunQueryTrigger( triggers.Types.beforeFind, @@ -121,7 +121,7 @@ function _UnsafeRestQuery( if (!this.auth.isMaster) { if (this.className == '_Session') { if (!this.auth.user) { - throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config); } this.restWhere = { $and: [ @@ -424,7 +424,8 @@ _UnsafeRestQuery.prototype.validateClientClassCreation = function () { if (hasClass !== true) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - 'This user is not allowed to access ' + 'non-existent class: ' + this.className + 'This user is not allowed to access ' + 'non-existent class: ' + this.className, + this.config ); } }); @@ -803,7 +804,8 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () { if (this.restWhere[key]) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - `This user is not allowed to query ${key} on class ${this.className}` + `This user is not allowed to query ${key} on class ${this.className}`, + this.config ); } } diff --git a/src/RestWrite.js b/src/RestWrite.js index c8c9584fde..a0de5577a5 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -33,6 +33,7 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, 'Cannot perform a write operation when using readOnlyMasterKey', + config ); } this.config = config; @@ -203,6 +204,7 @@ RestWrite.prototype.validateClientClassCreation = function () { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, 'This user is not allowed to access non-existent class: ' + this.className, + this.config ); } }); @@ -662,7 +664,8 @@ RestWrite.prototype.checkRestrictedFields = async function () { if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "Clients aren't allowed to manually update email verification." + "Clients aren't allowed to manually update email verification.", + this.config ); } }; @@ -1454,7 +1457,8 @@ RestWrite.prototype.runDatabaseOperation = function () { if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) { throw createSanitizedError( Parse.Error.SESSION_MISSING, - `Cannot modify user ${this.query.objectId}.` + `Cannot modify user ${this.query.objectId}.`, + this.config ); } diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 09dc85022b..83db6dbab0 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -112,7 +112,7 @@ export class ClassesRouter extends PromiseRouter { typeof req.body?.objectId === 'string' && req.body.objectId.startsWith('role:') ) { - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.'); + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.', req.config); } return rest.create( req.config, diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index e2b9271192..f0bb483d7b 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -5,7 +5,6 @@ import Config from '../Config'; import logger from '../logger'; const triggers = require('../triggers'); const Utils = require('../Utils'); -import { createSanitizedError } from '../Error'; export class FilesRouter { expressRouter({ maxUploadSize = '20Mb' } = {}) { @@ -44,8 +43,7 @@ export class FilesRouter { const config = Config.get(req.params.appId); if (!config) { res.status(403); - const err = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid application ID.'); - res.json({ code: err.code, error: err.message }); + res.json({ code: Parse.Error.OPERATION_FORBIDDEN, error: 'Invalid application ID.' }); return; } diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index 4b107ee878..6a05f7308f 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -45,6 +45,7 @@ export class GlobalConfigRouter extends PromiseRouter { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to update the config.", + req.config ); } const params = req.body.params || {}; diff --git a/src/Routers/GraphQLRouter.js b/src/Routers/GraphQLRouter.js index d785afbdf2..67d7a24e46 100644 --- a/src/Routers/GraphQLRouter.js +++ b/src/Routers/GraphQLRouter.js @@ -18,6 +18,7 @@ export class GraphQLRouter extends PromiseRouter { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to update the GraphQL config.", + req.config ); } const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {}); diff --git a/src/Routers/PurgeRouter.js b/src/Routers/PurgeRouter.js index 7b992a48e2..f346d64176 100644 --- a/src/Routers/PurgeRouter.js +++ b/src/Routers/PurgeRouter.js @@ -9,6 +9,7 @@ export class PurgeRouter extends PromiseRouter { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to purge a schema.", + req.config ); } return req.config.database diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js index 123677f138..696f19ed88 100644 --- a/src/Routers/PushRouter.js +++ b/src/Routers/PushRouter.js @@ -13,6 +13,7 @@ export class PushRouter extends PromiseRouter { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to send push notifications.", + req.config ); } const pushController = req.config.pushController; diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index ff55711a69..8713c95518 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -76,6 +76,7 @@ async function createSchema(req) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to create a schema.", + req.config ); } if (req.params.className && req.body?.className) { @@ -98,6 +99,7 @@ function modifySchema(req) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to update a schema.", + req.config ); } if (req.body?.className && req.body.className != req.params.className) { @@ -113,6 +115,7 @@ const deleteSchema = req => { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to delete a schema.", + req.config ); } if (!SchemaController.classNameIsValid(req.params.className)) { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index f50f9608d2..3828e465e7 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -172,7 +172,7 @@ export class UsersRouter extends ClassesRouter { handleMe(req) { if (!req.info || !req.info.sessionToken) { - throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config); } const sessionToken = req.info.sessionToken; return rest @@ -187,7 +187,7 @@ export class UsersRouter extends ClassesRouter { ) .then(response => { if (!response.results || response.results.length == 0 || !response.results[0].user) { - throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config); } else { const user = response.results[0].user; // Send token back on the login, because SDKs expect that. @@ -338,6 +338,7 @@ export class UsersRouter extends ClassesRouter { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, 'master key is required', + req.config ); } diff --git a/src/SharedRest.js b/src/SharedRest.js index 1d342b595a..3dc396d30c 100644 --- a/src/SharedRest.js +++ b/src/SharedRest.js @@ -9,12 +9,13 @@ const classesWithMasterOnlyAccess = [ const { createSanitizedError } = require('./Error'); // Disallowing access to the _Role collection except by master key -function enforceRoleSecurity(method, className, auth) { +function enforceRoleSecurity(method, className, auth, config) { if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) { if (method === 'delete' || method === 'find') { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - `Clients aren't allowed to perform the ${method} operation on the installation collection.` + `Clients aren't allowed to perform the ${method} operation on the installation collection.`, + config ); } } @@ -27,7 +28,8 @@ function enforceRoleSecurity(method, className, auth) { ) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - `Clients aren't allowed to perform the ${method} operation on the ${className} collection.` + `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`, + config ); } @@ -35,7 +37,8 @@ function enforceRoleSecurity(method, className, auth) { if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - `read-only masterKey isn't allowed to perform the ${method} operation.` + `read-only masterKey isn't allowed to perform the ${method} operation.`, + config ); } } diff --git a/src/middlewares.js b/src/middlewares.js index 2da7016b4f..2fedce8f08 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -502,7 +502,7 @@ export function handleParseErrors(err, req, res, next) { export function enforceMasterKeyAccess(req, res, next) { if (!req.auth.isMaster) { - const error = createSanitizedHttpError(403, 'unauthorized: master key is required'); + const error = createSanitizedHttpError(403, 'unauthorized: master key is required', req.config); res.status(error.status); res.end(`{"error":"${error.message}"}`); return; @@ -512,7 +512,7 @@ export function enforceMasterKeyAccess(req, res, next) { export function promiseEnforceMasterKeyAccess(request) { if (!request.auth.isMaster) { - throw createSanitizedHttpError(403, 'unauthorized: master key is required'); + throw createSanitizedHttpError(403, 'unauthorized: master key is required', request.config); } return Promise.resolve(); } diff --git a/src/rest.js b/src/rest.js index 66feae66f0..66763715ea 100644 --- a/src/rest.js +++ b/src/rest.js @@ -135,7 +135,7 @@ async function runFindTriggers( // Returns a promise for an object with optional keys 'results' and 'count'. const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => { - enforceRoleSecurity('find', className, auth); + enforceRoleSecurity('find', className, auth, config); return runFindTriggers( config, auth, @@ -150,7 +150,7 @@ const find = async (config, auth, className, restWhere, restOptions, clientSDK, // get is just like find but only queries an objectId. const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => { - enforceRoleSecurity('get', className, auth); + enforceRoleSecurity('get', className, auth, config); return runFindTriggers( config, auth, @@ -173,7 +173,7 @@ function del(config, auth, className, objectId, context) { throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth to delete user'); } - enforceRoleSecurity('delete', className, auth); + enforceRoleSecurity('delete', className, auth, config); let inflatedObject; let schemaController; @@ -196,7 +196,7 @@ function del(config, auth, className, objectId, context) { firstResult.className = className; if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) { if (!auth.user || firstResult.user.objectId !== auth.user.id) { - throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config); } } var cacheAdapter = config.cacheController; @@ -258,13 +258,13 @@ function del(config, auth, className, objectId, context) { ); }) .catch(error => { - handleSessionMissingError(error, className, auth); + handleSessionMissingError(error, className, auth, config); }); } // Returns a promise for a {response, status, location} object. function create(config, auth, className, restObject, clientSDK, context) { - enforceRoleSecurity('create', className, auth); + enforceRoleSecurity('create', className, auth, config); var write = new RestWrite(config, auth, className, null, restObject, null, clientSDK, context); return write.execute(); } @@ -273,7 +273,7 @@ function create(config, auth, className, restObject, clientSDK, context) { // REST API is supposed to return. // Usually, this is just updatedAt. function update(config, auth, className, restWhere, restObject, clientSDK, context) { - enforceRoleSecurity('update', className, auth); + enforceRoleSecurity('update', className, auth, config); return Promise.resolve() .then(async () => { @@ -315,11 +315,11 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte ).execute(); }) .catch(error => { - handleSessionMissingError(error, className, auth); + handleSessionMissingError(error, className, auth, config); }); } -function handleSessionMissingError(error, className, auth) { +function handleSessionMissingError(error, className, auth, config) { // If we're trying to update a user without / with bad session token if ( className === '_User' && @@ -327,7 +327,7 @@ function handleSessionMissingError(error, className, auth) { !auth.isMaster && !auth.isMaintenance ) { - throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.'); + throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.', config); } throw error; } From a906a11107bb2efdb80f36ad9706657b963380e9 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 28 Nov 2025 18:49:26 +0000 Subject: [PATCH 45/50] chore(release): 8.5.0-alpha.16 [skip ci] # [8.5.0-alpha.16](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.15...8.5.0-alpha.16) (2025-11-28) ### Features * Add Parse Server option `enableSanitizedErrorResponse` to remove detailed error messages from responses sent to clients ([#9944](https://github.com/parse-community/parse-server/issues/9944)) ([4752197](https://github.com/parse-community/parse-server/commit/47521974aeafcf41102be62f19612a4ab0a4837f)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index d0dd432adc..92db2b562e 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.16](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.15...8.5.0-alpha.16) (2025-11-28) + + +### Features + +* Add Parse Server option `enableSanitizedErrorResponse` to remove detailed error messages from responses sent to clients ([#9944](https://github.com/parse-community/parse-server/issues/9944)) ([4752197](https://github.com/parse-community/parse-server/commit/47521974aeafcf41102be62f19612a4ab0a4837f)) + # [8.5.0-alpha.15](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.14...8.5.0-alpha.15) (2025-11-23) diff --git a/package-lock.json b/package-lock.json index ee7f2cd789..b095f422ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.15", + "version": "8.5.0-alpha.16", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.15", + "version": "8.5.0-alpha.16", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 45a8bf12b0..82c77ea318 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.15", + "version": "8.5.0-alpha.16", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From fa57d69cbec525189da98d7136c1c0e9eaf74338 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:11:20 +0100 Subject: [PATCH 46/50] feat: Upgrade to parse 7.1.1 (#9954) --- package-lock.json | 53 +++++++---------------------------------------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index b095f422ae..04cbba7edf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "mongodb": "6.20.0", "mustache": "4.2.0", "otpauth": "9.4.0", - "parse": "7.0.1", + "parse": "7.1.1", "path-to-regexp": "6.3.0", "pg-monitor": "3.0.0", "pg-promise": "12.2.0", @@ -18688,22 +18688,17 @@ } }, "node_modules/parse": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/parse/-/parse-7.0.1.tgz", - "integrity": "sha512-6hCnE8EWky/MqDtlpMnztzL0BEEsU3jVI7iKl2+AlJeSAeWkCgkPcb30eBNq57FcCnqWWC6uVJAaUMmX3+zrvg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse/-/parse-7.1.1.tgz", + "integrity": "sha512-iqV+y9CqgJmh529chMuuswe8DSQYcUcF2bwnsA61WBpcI6W6kIQHt4avs+XlmEDMB0twYELdMET+QaKbUkBHBw==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "7.28.4", - "idb-keyval": "6.2.2", "react-native-crypto-js": "1.0.0", - "uuid": "10.0.0", "ws": "8.18.3" }, "engines": { - "node": "18 || 19 || 20 || 22" - }, - "optionalDependencies": { - "crypto-js": "4.2.0" + "node": "18 || 19 || 20 || 22 || 24" } }, "node_modules/parse-json": { @@ -18733,25 +18728,6 @@ "node": ">=6" } }, - "node_modules/parse/node_modules/idb-keyval": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", - "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", - "license": "Apache-2.0" - }, - "node_modules/parse/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/parse/node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -36073,28 +36049,15 @@ } }, "parse": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/parse/-/parse-7.0.1.tgz", - "integrity": "sha512-6hCnE8EWky/MqDtlpMnztzL0BEEsU3jVI7iKl2+AlJeSAeWkCgkPcb30eBNq57FcCnqWWC6uVJAaUMmX3+zrvg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse/-/parse-7.1.1.tgz", + "integrity": "sha512-iqV+y9CqgJmh529chMuuswe8DSQYcUcF2bwnsA61WBpcI6W6kIQHt4avs+XlmEDMB0twYELdMET+QaKbUkBHBw==", "requires": { "@babel/runtime-corejs3": "7.28.4", - "crypto-js": "4.2.0", - "idb-keyval": "6.2.2", "react-native-crypto-js": "1.0.0", - "uuid": "10.0.0", "ws": "8.18.3" }, "dependencies": { - "idb-keyval": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", - "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==" - }, - "uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==" - }, "ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", diff --git a/package.json b/package.json index 82c77ea318..9778de66f4 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "mongodb": "6.20.0", "mustache": "4.2.0", "otpauth": "9.4.0", - "parse": "7.0.1", + "parse": "7.1.1", "path-to-regexp": "6.3.0", "pg-monitor": "3.0.0", "pg-promise": "12.2.0", @@ -64,11 +64,11 @@ "ws": "8.18.2" }, "devDependencies": { - "@babel/eslint-parser": "7.28.0", "@actions/core": "1.11.1", "@apollo/client": "3.13.8", "@babel/cli": "7.27.0", "@babel/core": "7.27.4", + "@babel/eslint-parser": "7.28.0", "@babel/plugin-proposal-object-rest-spread": "7.20.7", "@babel/plugin-transform-flow-strip-types": "7.26.5", "@babel/preset-env": "7.27.2", From 0e1cb3367e4d89f667d526ec1a9b8cdf47db6a06 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 1 Dec 2025 18:12:18 +0000 Subject: [PATCH 47/50] chore(release): 8.5.0-alpha.17 [skip ci] # [8.5.0-alpha.17](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.16...8.5.0-alpha.17) (2025-12-01) ### Features * Upgrade to parse 7.1.1 ([#9954](https://github.com/parse-community/parse-server/issues/9954)) ([fa57d69](https://github.com/parse-community/parse-server/commit/fa57d69cbec525189da98d7136c1c0e9eaf74338)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 92db2b562e..56efad7977 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.17](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.16...8.5.0-alpha.17) (2025-12-01) + + +### Features + +* Upgrade to parse 7.1.1 ([#9954](https://github.com/parse-community/parse-server/issues/9954)) ([fa57d69](https://github.com/parse-community/parse-server/commit/fa57d69cbec525189da98d7136c1c0e9eaf74338)) + # [8.5.0-alpha.16](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.15...8.5.0-alpha.16) (2025-11-28) diff --git a/package-lock.json b/package-lock.json index 04cbba7edf..65e68fd868 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.16", + "version": "8.5.0-alpha.17", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.16", + "version": "8.5.0-alpha.17", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 9778de66f4..758eb540ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.16", + "version": "8.5.0-alpha.17", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 5c644a55ac25986f214b68ba4bcbe7a62ad6d6d1 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Mon, 1 Dec 2025 20:19:11 +0100 Subject: [PATCH 48/50] feat: Upgrade to parse 7.1.2 (#9955) --- package-lock.json | 35 ++++++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65e68fd868..9ffafe4e4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "mongodb": "6.20.0", "mustache": "4.2.0", "otpauth": "9.4.0", - "parse": "7.1.1", + "parse": "7.1.2", "path-to-regexp": "6.3.0", "pg-monitor": "3.0.0", "pg-promise": "12.2.0", @@ -9192,8 +9192,7 @@ "node_modules/crypto-js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", - "optional": true + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "node_modules/crypto-random-string": { "version": "4.0.0", @@ -18688,12 +18687,14 @@ } }, "node_modules/parse": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/parse/-/parse-7.1.1.tgz", - "integrity": "sha512-iqV+y9CqgJmh529chMuuswe8DSQYcUcF2bwnsA61WBpcI6W6kIQHt4avs+XlmEDMB0twYELdMET+QaKbUkBHBw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse/-/parse-7.1.2.tgz", + "integrity": "sha512-PXcPZDElBni06WPMxg0e6XmvgYBu3v39pRezZDbsomi8y9k1uNEDO/uICIqndw8jdES2ZfVpHp0TQoar2SObHQ==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "7.28.4", + "crypto-js": "4.2.0", + "idb-keyval": "6.2.2", "react-native-crypto-js": "1.0.0", "ws": "8.18.3" }, @@ -18728,6 +18729,12 @@ "node": ">=6" } }, + "node_modules/parse/node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0" + }, "node_modules/parse/node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -29430,8 +29437,7 @@ "crypto-js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", - "optional": true + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "crypto-random-string": { "version": "4.0.0", @@ -36049,15 +36055,22 @@ } }, "parse": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/parse/-/parse-7.1.1.tgz", - "integrity": "sha512-iqV+y9CqgJmh529chMuuswe8DSQYcUcF2bwnsA61WBpcI6W6kIQHt4avs+XlmEDMB0twYELdMET+QaKbUkBHBw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse/-/parse-7.1.2.tgz", + "integrity": "sha512-PXcPZDElBni06WPMxg0e6XmvgYBu3v39pRezZDbsomi8y9k1uNEDO/uICIqndw8jdES2ZfVpHp0TQoar2SObHQ==", "requires": { "@babel/runtime-corejs3": "7.28.4", + "crypto-js": "4.2.0", + "idb-keyval": "6.2.2", "react-native-crypto-js": "1.0.0", "ws": "8.18.3" }, "dependencies": { + "idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==" + }, "ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", diff --git a/package.json b/package.json index 758eb540ac..99c305d91a 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "mongodb": "6.20.0", "mustache": "4.2.0", "otpauth": "9.4.0", - "parse": "7.1.1", + "parse": "7.1.2", "path-to-regexp": "6.3.0", "pg-monitor": "3.0.0", "pg-promise": "12.2.0", From bdc71be9a3adef550deeef836e8e109dd3afccac Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 1 Dec 2025 19:19:59 +0000 Subject: [PATCH 49/50] chore(release): 8.5.0-alpha.18 [skip ci] # [8.5.0-alpha.18](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.17...8.5.0-alpha.18) (2025-12-01) ### Features * Upgrade to parse 7.1.2 ([#9955](https://github.com/parse-community/parse-server/issues/9955)) ([5c644a5](https://github.com/parse-community/parse-server/commit/5c644a55ac25986f214b68ba4bcbe7a62ad6d6d1)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 56efad7977..854b825c1f 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [8.5.0-alpha.18](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.17...8.5.0-alpha.18) (2025-12-01) + + +### Features + +* Upgrade to parse 7.1.2 ([#9955](https://github.com/parse-community/parse-server/issues/9955)) ([5c644a5](https://github.com/parse-community/parse-server/commit/5c644a55ac25986f214b68ba4bcbe7a62ad6d6d1)) + # [8.5.0-alpha.17](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.16...8.5.0-alpha.17) (2025-12-01) diff --git a/package-lock.json b/package-lock.json index 9ffafe4e4e..7ff3ddab63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "8.5.0-alpha.17", + "version": "8.5.0-alpha.18", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "8.5.0-alpha.17", + "version": "8.5.0-alpha.18", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 99c305d91a..163f13fb7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "8.5.0-alpha.17", + "version": "8.5.0-alpha.18", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 77bcdfc04e65a6d9be354a5a37d44140b0818af6 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 1 Dec 2025 19:21:23 +0000 Subject: [PATCH 50/50] empty commit to trigger CI