From 8cc71cf9e4a690df2a13878e9e72747ef5fa7ccb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:44:33 +0000 Subject: [PATCH 01/36] refactor: Bump @babel/core from 7.28.6 to 7.29.0 (#10032) --- package-lock.json | 122 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index c46e23133c..ea21aaa5df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "@actions/core": "1.11.1", "@apollo/client": "3.13.8", "@babel/cli": "7.27.0", - "@babel/core": "7.28.6", + "@babel/core": "7.29.0", "@babel/eslint-parser": "7.28.6", "@babel/plugin-proposal-object-rest-spread": "7.20.7", "@babel/plugin-transform-flow-strip-types": "7.26.5", @@ -497,9 +497,9 @@ "dev": true }, "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -520,20 +520,20 @@ } }, "node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -592,13 +592,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -919,12 +919,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -2192,17 +2192,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -2210,9 +2210,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -22819,9 +22819,9 @@ } }, "@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.28.5", @@ -22836,20 +22836,20 @@ "dev": true }, "@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "requires": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -22892,13 +22892,13 @@ } }, "@babel/generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "requires": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -23128,12 +23128,12 @@ } }, "@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "requires": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" } }, "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { @@ -23913,24 +23913,24 @@ } }, "@babel/traverse": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "requires": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "requires": { "@babel/helper-string-parser": "^7.27.1", diff --git a/package.json b/package.json index f01b480c2d..a573ea49d8 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@actions/core": "1.11.1", "@apollo/client": "3.13.8", "@babel/cli": "7.27.0", - "@babel/core": "7.28.6", + "@babel/core": "7.29.0", "@babel/eslint-parser": "7.28.6", "@babel/plugin-proposal-object-rest-spread": "7.20.7", "@babel/plugin-transform-flow-strip-types": "7.26.5", From e29910764daef3c03ed1b09eee19cedc3b12a86a Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 6 Feb 2026 01:42:54 +0000 Subject: [PATCH 02/36] fix: Default HTML pages for password reset, email verification not found (#10034) --- package.json | 2 +- spec/PagesRouter.spec.js | 55 ++++++++++++++++++++++++++++++++++++++ src/Routers/PagesRouter.js | 20 +++++++------- 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index a573ea49d8..57e9369fd4 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "files": [ "bin/", "lib/", - "public_html/", + "public/", "views/", "LICENSE", "NOTICE", diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 009254dfcc..fc32e61e13 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -1181,6 +1181,61 @@ describe('Pages Router', () => { }); }); + describe('async publicServerURL', () => { + it('resolves async publicServerURL for password reset page', async () => { + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + await reconfigureServer({ + appId: 'test', + appName: 'exampleAppname', + verifyUserEmails: true, + emailAdapter, + publicServerURL: () => 'http://localhost:8378/1', + pages: { enableRouter: true }, + }); + + const user = new Parse.User(); + user.setUsername('asyncUrlUser'); + user.setPassword('examplePassword'); + user.set('email', 'async-url@example.com'); + await user.signUp(); + await Parse.User.requestPasswordReset('async-url@example.com'); + + const response = await request({ + url: 'http://localhost:8378/1/apps/test/request_password_reset?token=invalidToken', + followRedirects: false, + }).catch(e => e); + expect(response.status).toBe(200); + expect(response.text).toContain('Invalid password reset link!'); + }); + + it('resolves async publicServerURL for email verification page', async () => { + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + await reconfigureServer({ + appId: 'test', + appName: 'exampleAppname', + verifyUserEmails: true, + emailAdapter, + publicServerURL: () => 'http://localhost:8378/1', + pages: { enableRouter: true }, + }); + + const response = await request({ + url: 'http://localhost:8378/1/apps/test/verify_email?token=invalidToken', + followRedirects: false, + }).catch(e => e); + expect(response.status).toBe(200); + expect(response.text).toContain('Invalid verification link!'); + }); + }); + describe('XSS Protection', () => { beforeEach(async () => { await reconfigureServer({ diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 74beec770c..a4f7c6f89b 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -624,12 +624,14 @@ export class PagesRouter extends PromiseRouter { * @param {Boolean} failGracefully Is true if failing to set the config should * not result in an invalid request response. Default is `false`. */ - setConfig(req, failGracefully = false) { + async setConfig(req, failGracefully = false) { req.config = Config.get(req.params.appId || req.query.appId); if (!req.config && !failGracefully) { this.invalidRequest(); } - return Promise.resolve(); + if (req.config) { + await req.config.loadKeys(); + } } mountPagesRoutes() { @@ -637,7 +639,7 @@ export class PagesRouter extends PromiseRouter { 'GET', `/${this.pagesEndpoint}/:appId/verify_email`, req => { - this.setConfig(req); + return this.setConfig(req); }, req => { return this.verifyEmail(req); @@ -648,7 +650,7 @@ export class PagesRouter extends PromiseRouter { 'POST', `/${this.pagesEndpoint}/:appId/resend_verification_email`, req => { - this.setConfig(req); + return this.setConfig(req); }, req => { return this.resendVerificationEmail(req); @@ -659,7 +661,7 @@ export class PagesRouter extends PromiseRouter { 'GET', `/${this.pagesEndpoint}/choose_password`, req => { - this.setConfig(req); + return this.setConfig(req); }, req => { return this.passwordReset(req); @@ -670,7 +672,7 @@ export class PagesRouter extends PromiseRouter { 'POST', `/${this.pagesEndpoint}/:appId/request_password_reset`, req => { - this.setConfig(req); + return this.setConfig(req); }, req => { return this.resetPassword(req); @@ -681,7 +683,7 @@ export class PagesRouter extends PromiseRouter { 'GET', `/${this.pagesEndpoint}/:appId/request_password_reset`, req => { - this.setConfig(req); + return this.setConfig(req); }, req => { return this.requestResetPassword(req); @@ -695,7 +697,7 @@ export class PagesRouter extends PromiseRouter { route.method, `/${this.pagesEndpoint}/:appId/${route.path}`, req => { - this.setConfig(req); + return this.setConfig(req); }, async req => { const { file, query = {} } = (await route.handler(req)) || {}; @@ -718,7 +720,7 @@ export class PagesRouter extends PromiseRouter { 'GET', `/${this.pagesEndpoint}/*resource`, req => { - this.setConfig(req, true); + return this.setConfig(req, true); }, req => { return this.staticRoute(req); From a4909792bd238ef69e91078990efe42df31ce3ac Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 6 Feb 2026 01:43:56 +0000 Subject: [PATCH 03/36] chore(release): 9.2.1-alpha.1 [skip ci] ## [9.2.1-alpha.1](https://github.com/parse-community/parse-server/compare/9.2.0...9.2.1-alpha.1) (2026-02-06) ### Bug Fixes * Default HTML pages for password reset, email verification not found ([#10034](https://github.com/parse-community/parse-server/issues/10034)) ([e299107](https://github.com/parse-community/parse-server/commit/e29910764daef3c03ed1b09eee19cedc3b12a86a)) --- 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 89be5e840a..98d401007f 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +## [9.2.1-alpha.1](https://github.com/parse-community/parse-server/compare/9.2.0...9.2.1-alpha.1) (2026-02-06) + + +### Bug Fixes + +* Default HTML pages for password reset, email verification not found ([#10034](https://github.com/parse-community/parse-server/issues/10034)) ([e299107](https://github.com/parse-community/parse-server/commit/e29910764daef3c03ed1b09eee19cedc3b12a86a)) + # [9.2.0-alpha.5](https://github.com/parse-community/parse-server/compare/9.2.0-alpha.4...9.2.0-alpha.5) (2026-02-05) diff --git a/package-lock.json b/package-lock.json index ea21aaa5df..beaa84d067 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.2.0", + "version": "9.2.1-alpha.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.2.0", + "version": "9.2.1-alpha.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 57e9369fd4..028d8195cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.2.0", + "version": "9.2.1-alpha.1", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From d3d6e9e22a212885690853cbbb84bb8c53da5646 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 02:03:34 +0000 Subject: [PATCH 04/36] fix: AuthData validation incorrectly triggered on unchanged providers (#10025) --- spec/AuthenticationAdaptersV2.spec.js | 73 +++++++++++++++++++++++++++ src/Auth.js | 27 +++++++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/spec/AuthenticationAdaptersV2.spec.js b/spec/AuthenticationAdaptersV2.spec.js index 7301ab54c1..e7bde12239 100644 --- a/spec/AuthenticationAdaptersV2.spec.js +++ b/spec/AuthenticationAdaptersV2.spec.js @@ -76,6 +76,41 @@ describe('Auth Adapter features', () => { validateAppId: () => Promise.resolve(), }; + // Code-based adapter that requires 'code' field (like gpgames) + const codeBasedAdapter = { + validateAppId: () => Promise.resolve(), + validateSetUp: authData => { + if (!authData.code) { + throw new Error('code is required.'); + } + return Promise.resolve({ save: { id: authData.id } }); + }, + validateUpdate: authData => { + if (!authData.code) { + throw new Error('code is required.'); + } + return Promise.resolve({ save: { id: authData.id } }); + }, + validateLogin: authData => { + if (!authData.code) { + throw new Error('code is required.'); + } + return Promise.resolve({ save: { id: authData.id } }); + }, + afterFind: authData => { + // Strip sensitive 'code' field when returning to client + return { id: authData.id }; + }, + }; + + // Simple adapter that doesn't require code + const simpleAdapter = { + validateAppId: () => Promise.resolve(), + validateSetUp: () => Promise.resolve(), + validateUpdate: () => Promise.resolve(), + validateLogin: () => Promise.resolve(), + }; + const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', @@ -1302,4 +1337,42 @@ describe('Auth Adapter features', () => { await user.fetch({ useMasterKey: true }); expect(user.get('authData')).toEqual({ adapterB: { id: 'test' } }); }); + + it('should handle multiple providers: add one while another remains unchanged (code-based)', async () => { + await reconfigureServer({ + auth: { + codeBasedAdapter, + simpleAdapter, + }, + }); + + // Login with code-based provider + const user = new Parse.User(); + await user.save({ authData: { codeBasedAdapter: { id: 'user1', code: 'code1' } } }); + const sessionToken = user.getSessionToken(); + await user.fetch({ sessionToken }); + + // At this point, authData.codeBasedAdapter only has {id: 'user1'} due to afterFind + const current = user.get('authData') || {}; + expect(current.codeBasedAdapter).toEqual({ id: 'user1' }); + + // Add a second provider while keeping the first unchanged + user.set('authData', { + ...current, + simpleAdapter: { id: 'simple1' }, + // codeBasedAdapter is NOT modified (no new code provided) + }); + + // This should succeed without requiring 'code' for codeBasedAdapter + await user.save(null, { sessionToken }); + + // Verify both providers are present + const reloaded = await new Parse.Query(Parse.User).get(user.id, { + useMasterKey: true, + }); + + const authData = reloaded.get('authData') || {}; + expect(authData.simpleAdapter && authData.simpleAdapter.id).toBe('simple1'); + expect(authData.codeBasedAdapter && authData.codeBasedAdapter.id).toBe('user1'); + }); }); diff --git a/src/Auth.js b/src/Auth.js index d8bf7e651f..0601151ca4 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -456,7 +456,32 @@ const hasMutatedAuthData = (authData, userAuthData) => { if (provider === 'anonymous') { return; } const providerData = authData[provider]; const userProviderAuthData = userAuthData[provider]; - if (!isDeepStrictEqual(providerData, userProviderAuthData)) { + + // If unlinking (setting to null), consider it mutated + if (providerData === null) { + mutatedAuthData[provider] = providerData; + return; + } + + // If provider doesn't exist in stored data, it's new + if (!userProviderAuthData) { + mutatedAuthData[provider] = providerData; + return; + } + + // Check if incoming data represents actual changes vs just echoing back + // what afterFind returned. If incoming data is a subset of stored data + // (all incoming fields match stored values), it's not mutated. + // If incoming data has different values or fields not in stored data, it's mutated. + // This handles the case where afterFind strips sensitive fields like 'code': + // - Incoming: { id: 'x' }, Stored: { id: 'x', code: 'secret' } -> NOT mutated (subset) + // - Incoming: { id: 'x', token: 'new' }, Stored: { id: 'x', token: 'old' } -> MUTATED + const incomingKeys = Object.keys(providerData || {}); + const hasChanges = incomingKeys.some(key => { + return !isDeepStrictEqual(providerData[key], userProviderAuthData[key]); + }); + + if (hasChanges) { mutatedAuthData[provider] = providerData; } }); From 617de9989b27025e876845ae544ce75b50b6977b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 6 Feb 2026 02:04:22 +0000 Subject: [PATCH 05/36] chore(release): 9.2.1-alpha.2 [skip ci] ## [9.2.1-alpha.2](https://github.com/parse-community/parse-server/compare/9.2.1-alpha.1...9.2.1-alpha.2) (2026-02-06) ### Bug Fixes * AuthData validation incorrectly triggered on unchanged providers ([#10025](https://github.com/parse-community/parse-server/issues/10025)) ([d3d6e9e](https://github.com/parse-community/parse-server/commit/d3d6e9e22a212885690853cbbb84bb8c53da5646)) --- 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 98d401007f..743d81db0b 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +## [9.2.1-alpha.2](https://github.com/parse-community/parse-server/compare/9.2.1-alpha.1...9.2.1-alpha.2) (2026-02-06) + + +### Bug Fixes + +* AuthData validation incorrectly triggered on unchanged providers ([#10025](https://github.com/parse-community/parse-server/issues/10025)) ([d3d6e9e](https://github.com/parse-community/parse-server/commit/d3d6e9e22a212885690853cbbb84bb8c53da5646)) + ## [9.2.1-alpha.1](https://github.com/parse-community/parse-server/compare/9.2.0...9.2.1-alpha.1) (2026-02-06) diff --git a/package-lock.json b/package-lock.json index beaa84d067..4a62aa2820 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.2.1-alpha.1", + "version": "9.2.1-alpha.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.2.1-alpha.1", + "version": "9.2.1-alpha.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 028d8195cd..77ac40eb4d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.2.1-alpha.1", + "version": "9.2.1-alpha.2", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From ed98c15f90f2fa6a66780941fd3705b805d6eb14 Mon Sep 17 00:00:00 2001 From: Palixir <73360179+coratgerl@users.noreply.github.com> Date: Fri, 6 Feb 2026 04:48:35 +0100 Subject: [PATCH 06/36] feat: Add event information to `verifyUserEmails`, `preventLoginWithUnverifiedEmail` to identify invoking signup / login action and auth provider (#9963) --- resources/buildConfigDefinitions.js | 5 + spec/EmailVerificationToken.spec.js | 159 ++++++++++++++++++++++- spec/ValidationAndPasswordsReset.spec.js | 1 + spec/buildConfigDefinitions.spec.js | 66 ++++++++++ spec/parsers.spec.js | 18 +++ src/Options/Definitions.js | 8 +- src/Options/docs.js | 4 +- src/Options/index.js | 36 ++++- src/Options/parsers.js | 8 ++ src/RestWrite.js | 34 ++++- src/Routers/UsersRouter.js | 16 +-- types/Options/index.d.ts | 22 +++- 12 files changed, 350 insertions(+), 27 deletions(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 8979e2658b..ae7246fd9b 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -158,6 +158,11 @@ function mapperFor(elt, t) { return wrap(t.identifier('booleanParser')); } else if (t.isObjectTypeAnnotation(elt)) { return wrap(t.identifier('objectParser')); + } else if (t.isUnionTypeAnnotation(elt)) { + const unionTypes = elt.typeAnnotation?.types || elt.types; + if (unionTypes?.some(type => t.isBooleanTypeAnnotation(type)) && unionTypes?.some(type => t.isFunctionTypeAnnotation(type))) { + return wrap(t.identifier('booleanOrFunctionParser')); + } } else if (t.isGenericTypeAnnotation(elt)) { const type = elt.typeAnnotation.id.name; if (type == 'Adapter') { diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index a0834196af..82114760af 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -288,7 +288,15 @@ describe('Email Verification Token Expiration:', () => { }; const verifyUserEmails = { method(req) { - expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']); + expect(Object.keys(req)).toEqual([ + 'original', + 'object', + 'master', + 'ip', + 'installationId', + 'createdWith', + ]); + expect(req.createdWith).toEqual({ action: 'signup', authProvider: 'password' }); return false; }, }; @@ -349,7 +357,15 @@ describe('Email Verification Token Expiration:', () => { }; const verifyUserEmails = { method(req) { - expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']); + expect(Object.keys(req)).toEqual([ + 'original', + 'object', + 'master', + 'ip', + 'installationId', + 'createdWith', + ]); + expect(req.createdWith).toEqual({ action: 'signup', authProvider: 'password' }); if (req.object.get('username') === 'no_email') { return false; } @@ -384,6 +400,144 @@ describe('Email Verification Token Expiration:', () => { expect(verifySpy).toHaveBeenCalledTimes(5); }); + it('provides createdWith on signup when verification blocks session creation', async () => { + const verifyUserEmails = { + method: params => { + expect(params.object).toBeInstanceOf(Parse.User); + expect(params.createdWith).toEqual({ action: 'signup', authProvider: 'password' }); + return true; + }, + }; + const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough(); + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: verifyUserEmails.method, + preventLoginWithUnverifiedEmail: true, + preventSignupWithUnverifiedEmail: true, + emailAdapter: MockEmailAdapterWithOptions({ + fromAddress: 'parse@example.com', + apiKey: 'k', + domain: 'd', + }), + publicServerURL: 'http://localhost:8378/1', + }); + + const user = new Parse.User(); + user.setUsername('signup_created_with'); + user.setPassword('pass'); + user.setEmail('signup@example.com'); + const res = await user.signUp().catch(e => e); + expect(res.message).toBe('User email is not verified.'); + expect(user.getSessionToken()).toBeUndefined(); + expect(verifySpy).toHaveBeenCalledTimes(2); // before signup completion and on preventLoginWithUnverifiedEmail + }); + + it('provides createdWith with auth provider on login verification', async () => { + const user = new Parse.User(); + user.setUsername('user_created_with_login'); + user.setPassword('pass'); + user.set('email', 'login@example.com'); + await user.signUp(); + + const verifyUserEmails = { + method: async params => { + expect(params.object).toBeInstanceOf(Parse.User); + expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' }); + return true; + }, + }; + const verifyUserEmailsSpy = spyOn(verifyUserEmails, 'method').and.callThrough(); + await reconfigureServer({ + appName: 'emailVerifyToken', + publicServerURL: 'http://localhost:8378/1', + verifyUserEmails: verifyUserEmails.method, + preventLoginWithUnverifiedEmail: verifyUserEmails.method, + preventSignupWithUnverifiedEmail: true, + emailAdapter: MockEmailAdapterWithOptions({ + fromAddress: 'parse@example.com', + apiKey: 'k', + domain: 'd', + }), + }); + + const res = await Parse.User.logIn('user_created_with_login', 'pass').catch(e => e); + expect(res.code).toBe(205); + expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(2); // before login completion and on preventLoginWithUnverifiedEmail + }); + + it('provides createdWith with auth provider on signup verification', async () => { + const createdWithValues = []; + const verifyUserEmails = { + method: params => { + createdWithValues.push(params.createdWith); + return true; + }, + }; + const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough(); + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: verifyUserEmails.method, + preventLoginWithUnverifiedEmail: true, + preventSignupWithUnverifiedEmail: true, + emailAdapter: MockEmailAdapterWithOptions({ + fromAddress: 'parse@example.com', + apiKey: 'k', + domain: 'd', + }), + publicServerURL: 'http://localhost:8378/1', + }); + + const provider = { + authData: { id: '8675309', access_token: 'jenny' }, + shouldError: false, + authenticate(options) { + options.success(this, this.authData); + }, + restoreAuthentication() { + return true; + }, + getAuthType() { + return 'facebook'; + }, + deauthenticate() {}, + }; + Parse.User._registerAuthenticationProvider(provider); + const res = await Parse.User._logInWith('facebook').catch(e => e); + expect(res.message).toBe('User email is not verified.'); + // Called once in createSessionTokenIfNeeded (no email set, so _validateEmail skips) + expect(verifySpy).toHaveBeenCalledTimes(1); + expect(createdWithValues[0]).toEqual({ action: 'signup', authProvider: 'facebook' }); + }); + + it('provides createdWith for preventLoginWithUnverifiedEmail function', async () => { + const user = new Parse.User(); + user.setUsername('user_prevent_login_fn'); + user.setPassword('pass'); + user.set('email', 'preventlogin@example.com'); + await user.signUp(); + + const preventLoginCreatedWith = []; + await reconfigureServer({ + appName: 'emailVerifyToken', + publicServerURL: 'http://localhost:8378/1', + verifyUserEmails: true, + preventLoginWithUnverifiedEmail: params => { + preventLoginCreatedWith.push(params.createdWith); + return true; + }, + emailAdapter: MockEmailAdapterWithOptions({ + fromAddress: 'parse@example.com', + apiKey: 'k', + domain: 'd', + }), + }); + + const res = await Parse.User.logIn('user_prevent_login_fn', 'pass').catch(e => e); + expect(res.code).toBe(205); + expect(preventLoginCreatedWith.length).toBe(1); + expect(preventLoginCreatedWith[0]).toEqual({ action: 'login', authProvider: 'password' }); + }); + it_id('d812de87-33d1-495e-a6e8-3485f6dc3589')(it)('can conditionally send user email verification', async () => { const emailAdapter = { sendVerificationEmail: () => {}, @@ -779,6 +933,7 @@ describe('Email Verification Token Expiration:', () => { expect(params.master).toBeDefined(); expect(params.installationId).toBeDefined(); expect(params.resendRequest).toBeTrue(); + expect(params.createdWith).toBeUndefined(); return true; }, }; diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index e2113c2804..62d00275e7 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -284,6 +284,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { expect(params.ip).toBeDefined(); expect(params.master).toBeDefined(); expect(params.installationId).toBeDefined(); + expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' }); return true; }, }; diff --git a/spec/buildConfigDefinitions.spec.js b/spec/buildConfigDefinitions.spec.js index f0a8055860..bc15793a04 100644 --- a/spec/buildConfigDefinitions.spec.js +++ b/spec/buildConfigDefinitions.spec.js @@ -133,6 +133,72 @@ describe('buildConfigDefinitions', () => { expect(result.property.name).toBe('arrayParser'); }); + it('should return booleanOrFunctionParser for UnionTypeAnnotation containing boolean (nullable)', () => { + const mockElement = { + type: 'UnionTypeAnnotation', + typeAnnotation: { + types: [ + { type: 'BooleanTypeAnnotation' }, + { type: 'FunctionTypeAnnotation' }, + ], + }, + }; + + const result = mapperFor(mockElement, t); + + expect(t.isMemberExpression(result)).toBe(true); + expect(result.object.name).toBe('parsers'); + expect(result.property.name).toBe('booleanOrFunctionParser'); + }); + + it('should return booleanOrFunctionParser for UnionTypeAnnotation containing boolean (non-nullable)', () => { + const mockElement = { + type: 'UnionTypeAnnotation', + types: [ + { type: 'BooleanTypeAnnotation' }, + { type: 'FunctionTypeAnnotation' }, + ], + }; + + const result = mapperFor(mockElement, t); + + expect(t.isMemberExpression(result)).toBe(true); + expect(result.object.name).toBe('parsers'); + expect(result.property.name).toBe('booleanOrFunctionParser'); + }); + + it('should return undefined for UnionTypeAnnotation without boolean', () => { + const mockElement = { + type: 'UnionTypeAnnotation', + typeAnnotation: { + types: [ + { type: 'StringTypeAnnotation' }, + { type: 'NumberTypeAnnotation' }, + ], + }, + }; + + const result = mapperFor(mockElement, t); + + expect(result).toBeUndefined(); + }); + + it('should return undefined for UnionTypeAnnotation with boolean but without function', () => { + const mockElement = { + type: 'UnionTypeAnnotation', + typeAnnotation: { + types: [ + { type: 'BooleanTypeAnnotation' }, + { type: 'VoidTypeAnnotation' }, + ], + }, + }; + + const result = mapperFor(mockElement, t); + + expect(result).toBeUndefined(); + }); + it('should return objectParser for unknown GenericTypeAnnotation', () => { const mockElement = { type: 'GenericTypeAnnotation', diff --git a/spec/parsers.spec.js b/spec/parsers.spec.js index 413bdb5156..a844016ba7 100644 --- a/spec/parsers.spec.js +++ b/spec/parsers.spec.js @@ -3,6 +3,7 @@ const { numberOrBoolParser, numberOrStringParser, booleanParser, + booleanOrFunctionParser, objectParser, arrayParser, moduleOrObjectParser, @@ -48,6 +49,23 @@ describe('parsers', () => { expect(parser(2)).toEqual(false); }); + it('parses correctly with booleanOrFunctionParser', () => { + const parser = booleanOrFunctionParser; + // Preserves functions + const fn = () => true; + expect(parser(fn)).toBe(fn); + const asyncFn = async () => false; + expect(parser(asyncFn)).toBe(asyncFn); + // Parses booleans and string booleans like booleanParser + expect(parser(true)).toEqual(true); + expect(parser(false)).toEqual(false); + expect(parser('true')).toEqual(true); + expect(parser('false')).toEqual(false); + expect(parser('1')).toEqual(true); + expect(parser(1)).toEqual(true); + expect(parser(0)).toEqual(false); + }); + it('parses correctly with objectParser', () => { const parser = objectParser; expect(parser({ hello: 'world' })).toEqual({ hello: 'world' }); diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 973e8b301b..86104e5e15 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -473,8 +473,8 @@ module.exports.ParseServerOptions = { preventLoginWithUnverifiedEmail: { env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL', help: - '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`.', - action: parsers.booleanParser, + "Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

The `createdWith` values per scenario:Default is `false`.
Requires option `verifyUserEmails: true`.", + action: parsers.booleanOrFunctionParser, default: false, }, preventSignupWithUnverifiedEmail: { @@ -574,6 +574,7 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION', help: 'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.

Default is `true`.
', + action: parsers.booleanOrFunctionParser, default: true, }, serverCloseComplete: { @@ -630,7 +631,8 @@ module.exports.ParseServerOptions = { verifyUserEmails: { env: 'PARSE_SERVER_VERIFY_USER_EMAILS', help: - 'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.

Default is `false`.', + "Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

The `createdWith` values per scenario:Default is `false`.", + action: parsers.booleanOrFunctionParser, default: false, }, webhookKey: { diff --git a/src/Options/docs.js b/src/Options/docs.js index fd553db46d..0240d14b16 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -84,7 +84,7 @@ * @property {String} playgroundPath Mount path for the GraphQL Playground, defaults to /playground * @property {Number} port The port to run the ParseServer, defaults to 1337. * @property {Boolean} preserveFileName Enable (or disable) the addition of a unique hash to the file names - * @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} preventLoginWithUnverifiedEmail Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

The `createdWith` values per scenario: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 {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://`. @@ -108,7 +108,7 @@ * @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields * @property {Boolean} verbose Set the logging to verbose * @property {Boolean} verifyServerUrl Parse Server makes a HTTP request to the URL set in `serverURL` at the end of its launch routine to verify that the launch succeeded. If this option is set to `false`, the verification will be skipped. This can be useful in environments where the server URL is not accessible from the server itself, such as when running behind a firewall or in certain containerized environments.

⚠️ Server URL verification requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.

Default is `true`. - * @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.

Default is `false`. + * @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

The `createdWith` values per scenario:Default is `false`. * @property {String} webhookKey Key sent with outgoing webhook calls */ diff --git a/src/Options/index.js b/src/Options/index.js index 5278059ee0..e190d376da 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -43,6 +43,22 @@ type RequestKeywordDenylist = { key: string | any, value: any, }; +type EmailVerificationRequest = { + original?: any, + object: any, + master?: boolean, + ip?: string, + installationId?: string, + createdWith?: { + action: 'login' | 'signup', + authProvider: string, + }, + resendRequest?: boolean, +}; +type SendEmailVerificationRequest = { + user: any, + master?: boolean, +}; export interface ParseServerOptions { /* Your Parse Application ID @@ -174,18 +190,25 @@ export interface ParseServerOptions { /* Max file size for uploads, defaults to 20mb :DEFAULT: 20mb */ maxUploadSize: ?string; - /* Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. + /* Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

+ The `createdWith` values per scenario: + Default is `false`. :DEFAULT: false */ - verifyUserEmails: ?(boolean | void); - /* Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. + verifyUserEmails: ?(boolean | (EmailVerificationRequest => boolean | Promise)); + /* Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

+ The `createdWith` values per scenario: +
  • Password signup: `{ action: 'signup', authProvider: 'password' }`
  • Auth provider signup: `{ action: 'signup', authProvider: '' }`
  • Password login: `{ action: 'login', authProvider: 'password' }`
  • Auth provider login: function not invoked; auth provider login bypasses email verification
Default is `false`.
Requires option `verifyUserEmails: true`. :DEFAULT: false */ - preventLoginWithUnverifiedEmail: ?boolean; + preventLoginWithUnverifiedEmail: ?( + | boolean + | (EmailVerificationRequest => boolean | Promise) + ); /* 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`. @@ -214,7 +237,10 @@ export interface ParseServerOptions { Default is `true`.
:DEFAULT: true */ - sendUserEmailVerification: ?(boolean | void); + sendUserEmailVerification: ?( + | boolean + | (SendEmailVerificationRequest => boolean | Promise) + ); /* The account lockout policy for failed login attempts. */ accountLockout: ?AccountLockoutOptions; /* The password policy for enforcing password related rules. */ diff --git a/src/Options/parsers.js b/src/Options/parsers.js index 3fdad89dc3..d2f0b7b6fb 100644 --- a/src/Options/parsers.js +++ b/src/Options/parsers.js @@ -68,6 +68,13 @@ function booleanParser(opt) { return false; } +function booleanOrFunctionParser(opt) { + if (typeof opt === 'function') { + return opt; + } + return booleanParser(opt); +} + function nullParser(opt) { if (opt == 'null') { return null; @@ -81,6 +88,7 @@ module.exports = { numberOrStringParser, nullParser, booleanParser, + booleanOrFunctionParser, moduleOrObjectParser, arrayParser, objectParser, diff --git a/src/RestWrite.js b/src/RestWrite.js index a0de5577a5..6630a81e85 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -771,6 +771,30 @@ RestWrite.prototype._validateUserName = function () { }); }; +RestWrite.buildCreatedWith = function (action, authProvider) { + return { action, authProvider: authProvider || 'password' }; +}; + +RestWrite.prototype.getCreatedWith = function () { + if (this.storage.createdWith) { + return this.storage.createdWith; + } + const isCreateOperation = !this.query; + const authDataProvider = + this.data?.authData && + Object.keys(this.data.authData).length && + Object.keys(this.data.authData).join(','); + const authProvider = this.storage.authProvider || authDataProvider; + // storage.authProvider is only set for login (existing user found in handleAuthData) + const action = this.storage.authProvider ? 'login' : isCreateOperation ? 'signup' : undefined; + if (!action) { + return; + } + const resolvedAuthProvider = authProvider || (action === 'signup' ? 'password' : undefined); + this.storage.createdWith = RestWrite.buildCreatedWith(action, resolvedAuthProvider); + return this.storage.createdWith; +}; + /* As with usernames, Parse should not allow case insensitive collisions of email. unlike with usernames (which can have case insensitive collisions in the case of @@ -826,6 +850,7 @@ RestWrite.prototype._validateEmail = function () { master: this.auth.isMaster, ip: this.config.ip, installationId: this.auth.installationId, + createdWith: this.getCreatedWith(), }; return this.config.userController.setEmailVerifyToken(this.data, request, this.storage); } @@ -961,6 +986,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () { master: this.auth.isMaster, ip: this.config.ip, installationId: this.auth.installationId, + createdWith: this.getCreatedWith(), }; // Get verification conditions which can be booleans or functions; the purpose of this async/await // structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the @@ -985,14 +1011,14 @@ RestWrite.prototype.createSessionToken = async function () { if (this.storage.authProvider == null && this.data.authData) { this.storage.authProvider = Object.keys(this.data.authData).join(','); + // Invalidate cached createdWith since authProvider was just resolved + delete this.storage.createdWith; } + const createdWith = this.getCreatedWith(); const { sessionData, createSession } = RestWrite.createSession(this.config, { userId: this.objectId(), - createdWith: { - action: this.storage.authProvider ? 'login' : 'signup', - authProvider: this.storage.authProvider || 'password', - }, + createdWith, installationId: this.auth.installationId, }); diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 3828e465e7..6421d9abe1 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -140,11 +140,17 @@ export class UsersRouter extends ClassesRouter { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); } // Create request object for verification functions + const authProvider = + req.body && + req.body.authData && + Object.keys(req.body.authData).length && + Object.keys(req.body.authData).join(','); const request = { master: req.auth.isMaster, ip: req.config.ip, installationId: req.auth.installationId, object: Parse.User.fromJSON(Object.assign({ className: '_User' }, user)), + createdWith: RestWrite.buildCreatedWith('login', authProvider), }; // If request doesn't use master or maintenance key with ignoring email verification @@ -290,10 +296,7 @@ export class UsersRouter extends ClassesRouter { const { sessionData, createSession } = RestWrite.createSession(req.config, { userId: user.objectId, - createdWith: { - action: 'login', - authProvider: 'password', - }, + createdWith: RestWrite.buildCreatedWith('login'), installationId: req.info.installationId, }); @@ -360,10 +363,7 @@ export class UsersRouter extends ClassesRouter { const { sessionData, createSession } = RestWrite.createSession(req.config, { userId, - createdWith: { - action: 'login', - authProvider: 'masterkey', - }, + createdWith: RestWrite.buildCreatedWith('login', 'masterkey'), installationId: req.info.installationId, }); diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index 8cee0f6ea5..e0c1bbc1ec 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -26,6 +26,22 @@ type RequestKeywordDenylist = { key: string; value: any; }; +export interface EmailVerificationRequest { + original?: any; + object: any; + master?: boolean; + ip?: string; + installationId?: string; + createdWith?: { + action: 'login' | 'signup'; + authProvider: string; + }; + resendRequest?: boolean; +} +export interface SendEmailVerificationRequest { + user: any; + master?: boolean; +} export interface ParseServerOptions { appId: string; masterKey: (() => void) | string; @@ -74,12 +90,12 @@ export interface ParseServerOptions { auth?: Record; enableInsecureAuthAdapters?: boolean; maxUploadSize?: string; - verifyUserEmails?: (boolean | void); - preventLoginWithUnverifiedEmail?: boolean; + verifyUserEmails?: boolean | ((params: EmailVerificationRequest) => boolean | Promise); + preventLoginWithUnverifiedEmail?: boolean | ((params: EmailVerificationRequest) => boolean | Promise); preventSignupWithUnverifiedEmail?: boolean; emailVerifyTokenValidityDuration?: number; emailVerifyTokenReuseIfValid?: boolean; - sendUserEmailVerification?: (boolean | void); + sendUserEmailVerification?: boolean | ((params: SendEmailVerificationRequest) => boolean | Promise); accountLockout?: AccountLockoutOptions; passwordPolicy?: PasswordPolicyOptions; cacheAdapter?: Adapter; From 27b27a7f5cb1574395be8f445efad4f05bc7fc49 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 6 Feb 2026 03:49:32 +0000 Subject: [PATCH 07/36] chore(release): 9.3.0-alpha.1 [skip ci] # [9.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/9.2.1-alpha.2...9.3.0-alpha.1) (2026-02-06) ### Features * Add event information to `verifyUserEmails`, `preventLoginWithUnverifiedEmail` to identify invoking signup / login action and auth provider ([#9963](https://github.com/parse-community/parse-server/issues/9963)) ([ed98c15](https://github.com/parse-community/parse-server/commit/ed98c15f90f2fa6a66780941fd3705b805d6eb14)) --- 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 743d81db0b..92adb92e90 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [9.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/9.2.1-alpha.2...9.3.0-alpha.1) (2026-02-06) + + +### Features + +* Add event information to `verifyUserEmails`, `preventLoginWithUnverifiedEmail` to identify invoking signup / login action and auth provider ([#9963](https://github.com/parse-community/parse-server/issues/9963)) ([ed98c15](https://github.com/parse-community/parse-server/commit/ed98c15f90f2fa6a66780941fd3705b805d6eb14)) + ## [9.2.1-alpha.2](https://github.com/parse-community/parse-server/compare/9.2.1-alpha.1...9.2.1-alpha.2) (2026-02-06) diff --git a/package-lock.json b/package-lock.json index 4a62aa2820..7c6afbb4ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.2.1-alpha.2", + "version": "9.3.0-alpha.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.2.1-alpha.2", + "version": "9.3.0-alpha.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 77ac40eb4d..46ca421b73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.2.1-alpha.2", + "version": "9.3.0-alpha.1", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From c1f1800cade51cf9d90f1b2bcb81e82249082ff6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 15:19:51 +0000 Subject: [PATCH 08/36] refactor: Bump commander from 14.0.2 to 14.0.3 (#10039) --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c6afbb4ea..bbcd880d7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@parse/fs-files-adapter": "3.0.0", "@parse/push-adapter": "8.2.0", "bcryptjs": "3.0.3", - "commander": "14.0.2", + "commander": "14.0.3", "cors": "2.8.6", "deepcopy": "2.1.0", "express": "5.2.1", @@ -8555,9 +8555,9 @@ } }, "node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "engines": { "node": ">=20" } @@ -28403,9 +28403,9 @@ } }, "commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==" + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==" }, "commondir": { "version": "1.0.1", diff --git a/package.json b/package.json index 46ca421b73..bc12d33aa0 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@parse/fs-files-adapter": "3.0.0", "@parse/push-adapter": "8.2.0", "bcryptjs": "3.0.3", - "commander": "14.0.2", + "commander": "14.0.3", "cors": "2.8.6", "deepcopy": "2.1.0", "express": "5.2.1", From a4265bb1241551b7147e8aee08c36e1f8ab09ba4 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:30:13 +0000 Subject: [PATCH 09/36] fix: Default HTML pages for password reset, email verification not found (#10041) --- spec/PagesRouter.spec.js | 34 +++++++++++++++++++++++++++++++--- src/Config.js | 4 +--- src/Options/Definitions.js | 3 +-- src/Options/docs.js | 2 +- src/Options/index.js | 3 +-- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index fc32e61e13..db2250f15a 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -225,9 +225,7 @@ describe('Pages Router', () => { expect(Config.get(Parse.applicationId).pages.forceRedirect).toBe( Definitions.PagesOptions.forceRedirect.default ); - expect(Config.get(Parse.applicationId).pages.pagesPath).toBe( - Definitions.PagesOptions.pagesPath.default - ); + expect(Config.get(Parse.applicationId).pages.pagesPath).toBeUndefined(); expect(Config.get(Parse.applicationId).pages.pagesEndpoint).toBe( Definitions.PagesOptions.pagesEndpoint.default ); @@ -1236,6 +1234,36 @@ describe('Pages Router', () => { }); }); + describe('pagesPath resolution', () => { + it('should serve pages when current working directory differs from module directory', async () => { + const originalCwd = process.cwd(); + const os = require('os'); + process.chdir(os.tmpdir()); + + try { + await reconfigureServer({ + appId: 'test', + appName: 'exampleAppname', + publicServerURL: 'http://localhost:8378/1', + pages: { enableRouter: true }, + }); + + // Request the password reset page with an invalid token; + // even with an invalid token, the server should serve the + // "invalid link" page (200), not a 404. A 404 indicates the + // HTML template files could not be found because pagesPath + // resolved to the wrong directory. + const response = await request({ + url: 'http://localhost:8378/1/apps/test/request_password_reset?token=invalidToken', + }).catch(e => e); + expect(response.status).toBe(200); + expect(response.text).toContain('Invalid password reset link'); + } finally { + process.chdir(originalCwd); + } + }); + }); + describe('XSS Protection', () => { beforeEach(async () => { await reconfigureServer({ diff --git a/src/Config.js b/src/Config.js index 54e3cc5ca4..9b8d31a58b 100644 --- a/src/Config.js +++ b/src/Config.js @@ -326,9 +326,7 @@ export class Config { } else if (!isBoolean(pages.forceRedirect)) { throw 'Parse Server option pages.forceRedirect must be a boolean.'; } - if (pages.pagesPath === undefined) { - pages.pagesPath = PagesOptions.pagesPath.default; - } else if (!isString(pages.pagesPath)) { + if (pages.pagesPath !== undefined && !isString(pages.pagesPath)) { throw 'Parse Server option pages.pagesPath must be a string.'; } if (pages.pagesEndpoint === undefined) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 86104e5e15..59d70a3ae8 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -772,8 +772,7 @@ module.exports.PagesOptions = { pagesPath: { env: 'PARSE_SERVER_PAGES_PAGES_PATH', help: - "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory.", - default: './public', + "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory of the parse-server module.", }, placeholders: { env: 'PARSE_SERVER_PAGES_PLACEHOLDERS', diff --git a/src/Options/docs.js b/src/Options/docs.js index 0240d14b16..1f63cdda2e 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -142,7 +142,7 @@ * @property {String} localizationFallbackLocale The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. * @property {String} localizationJsonPath The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. * @property {String} pagesEndpoint The API endpoint for the pages. Default is 'apps'. - * @property {String} pagesPath The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. + * @property {String} pagesPath The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory of the parse-server module. * @property {Object} placeholders The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function. */ diff --git a/src/Options/index.js b/src/Options/index.js index e190d376da..fd2b00e3c6 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -437,8 +437,7 @@ export interface PagesOptions { /* Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). :DEFAULT: false */ forceRedirect: ?boolean; - /* The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory. - :DEFAULT: ./public */ + /* The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory of the parse-server module. */ pagesPath: ?string; /* The API endpoint for the pages. Default is 'apps'. :DEFAULT: apps */ From 97de70a01782b99ebba99211ecb2cfd1979fa010 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 6 Feb 2026 16:31:03 +0000 Subject: [PATCH 10/36] chore(release): 9.3.0-alpha.2 [skip ci] # [9.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.1...9.3.0-alpha.2) (2026-02-06) ### Bug Fixes * Default HTML pages for password reset, email verification not found ([#10041](https://github.com/parse-community/parse-server/issues/10041)) ([a4265bb](https://github.com/parse-community/parse-server/commit/a4265bb1241551b7147e8aee08c36e1f8ab09ba4)) --- 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 92adb92e90..91f949a1e8 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [9.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.1...9.3.0-alpha.2) (2026-02-06) + + +### Bug Fixes + +* Default HTML pages for password reset, email verification not found ([#10041](https://github.com/parse-community/parse-server/issues/10041)) ([a4265bb](https://github.com/parse-community/parse-server/commit/a4265bb1241551b7147e8aee08c36e1f8ab09ba4)) + # [9.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/9.2.1-alpha.2...9.3.0-alpha.1) (2026-02-06) diff --git a/package-lock.json b/package-lock.json index bbcd880d7b..cb55e39948 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.3.0-alpha.1", + "version": "9.3.0-alpha.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.3.0-alpha.1", + "version": "9.3.0-alpha.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index bc12d33aa0..d3ce6fc464 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.3.0-alpha.1", + "version": "9.3.0-alpha.2", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 558e1a32043b865344b92f62cbd7f9a0d82b20fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:42:52 +0000 Subject: [PATCH 11/36] refactor: Bump @semantic-release/release-notes-generator from 14.0.3 to 14.1.0 (#10038) --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb55e39948..01b635ee5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "@semantic-release/git": "10.0.1", "@semantic-release/github": "11.0.3", "@semantic-release/npm": "12.0.1", - "@semantic-release/release-notes-generator": "14.0.3", + "@semantic-release/release-notes-generator": "14.1.0", "all-node-versions": "13.0.1", "apollo-upload-client": "18.0.1", "clean-jsdoc-theme": "4.3.0", @@ -5814,9 +5814,9 @@ } }, "node_modules/@semantic-release/release-notes-generator": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-14.0.3.tgz", - "integrity": "sha512-XxAZRPWGwO5JwJtS83bRdoIhCiYIx8Vhr+u231pQAsdFIAbm19rSVJLdnBN+Avvk7CKvNQE/nJ4y7uqKH6WTiw==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-14.1.0.tgz", + "integrity": "sha512-CcyDRk7xq+ON/20YNR+1I/jP7BYKICr1uKd1HHpROSnnTdGqOTburi4jcRiTYz0cpfhxSloQO3cGhnoot7IEkA==", "dev": true, "dependencies": { "conventional-changelog-angular": "^8.0.0", @@ -26486,9 +26486,9 @@ } }, "@semantic-release/release-notes-generator": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-14.0.3.tgz", - "integrity": "sha512-XxAZRPWGwO5JwJtS83bRdoIhCiYIx8Vhr+u231pQAsdFIAbm19rSVJLdnBN+Avvk7CKvNQE/nJ4y7uqKH6WTiw==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-14.1.0.tgz", + "integrity": "sha512-CcyDRk7xq+ON/20YNR+1I/jP7BYKICr1uKd1HHpROSnnTdGqOTburi4jcRiTYz0cpfhxSloQO3cGhnoot7IEkA==", "dev": true, "requires": { "conventional-changelog-angular": "^8.0.0", diff --git a/package.json b/package.json index d3ce6fc464..d0491461fb 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "@semantic-release/git": "10.0.1", "@semantic-release/github": "11.0.3", "@semantic-release/npm": "12.0.1", - "@semantic-release/release-notes-generator": "14.0.3", + "@semantic-release/release-notes-generator": "14.1.0", "all-node-versions": "13.0.1", "apollo-upload-client": "18.0.1", "clean-jsdoc-theme": "4.3.0", From 9e07ca6d3ba45a7aec462752ae1daa0aeb1b7318 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 7 Feb 2026 01:11:09 +0000 Subject: [PATCH 12/36] refactor: Bump prettier from 2.0.5 to 3.8.1 (#10042) --- package-lock.json | 22 +- package.json | 2 +- src/Options/Definitions.js | 405 +++++++++++++------------------------ 3 files changed, 149 insertions(+), 280 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01b635ee5d..32a39338cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,7 +98,7 @@ "node-abort-controller": "3.1.1", "node-fetch": "3.2.10", "nyc": "17.1.0", - "prettier": "2.0.5", + "prettier": "3.8.1", "semantic-release": "24.2.5", "typescript": "5.8.3", "typescript-eslint": "8.53.1", @@ -18772,15 +18772,19 @@ } }, "node_modules/prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, + "license": "MIT", "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/pretty-ms": { @@ -35479,9 +35483,9 @@ "dev": true }, "prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true }, "pretty-ms": { diff --git a/package.json b/package.json index d0491461fb..37ffc08cf9 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "node-abort-controller": "3.1.1", "node-fetch": "3.2.10", "nyc": "17.1.0", - "prettier": "2.0.5", + "prettier": "3.8.1", "semantic-release": "24.2.5", "typescript": "5.8.3", "typescript-eslint": "8.53.1", diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 59d70a3ae8..61ce28caec 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -15,37 +15,32 @@ module.exports.SchemaOptions = { }, definitions: { env: 'PARSE_SERVER_SCHEMA_DEFINITIONS', - help: - 'Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema', + help: 'Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema', required: true, action: parsers.objectParser, default: [], }, deleteExtraFields: { env: 'PARSE_SERVER_SCHEMA_DELETE_EXTRA_FIELDS', - help: - 'Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.', + help: 'Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.', action: parsers.booleanParser, default: false, }, keepUnknownIndexes: { env: 'PARSE_SERVER_SCHEMA_KEEP_UNKNOWN_INDEXES', - help: - "(Optional) Keep indexes that are present in the database but not defined in the schema. Set this to `true` if you are adding indexes manually, so that they won't be removed when running schema migration. Default is `false`.", + help: "(Optional) Keep indexes that are present in the database but not defined in the schema. Set this to `true` if you are adding indexes manually, so that they won't be removed when running schema migration. Default is `false`.", action: parsers.booleanParser, default: false, }, lockSchemas: { env: 'PARSE_SERVER_SCHEMA_LOCK_SCHEMAS', - help: - 'Is true if Parse Server will reject any attempts to modify the schema while the server is running.', + help: 'Is true if Parse Server will reject any attempts to modify the schema while the server is running.', action: parsers.booleanParser, default: false, }, recreateModifiedFields: { env: 'PARSE_SERVER_SCHEMA_RECREATE_MODIFIED_FIELDS', - help: - 'Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.', + help: 'Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.', action: parsers.booleanParser, default: false, }, @@ -77,8 +72,7 @@ module.exports.ParseServerOptions = { }, allowExpiredAuthDataToken: { env: 'PARSE_SERVER_ALLOW_EXPIRED_AUTH_DATA_TOKEN', - help: - 'Allow a user to log in even if the 3rd party authentication token that was used to sign in to their account has expired. If this is set to `false`, then the token will be validated every time the user signs in to their account. This refers to the token that is stored in the `_User.authData` field. Defaults to `false`.', + help: 'Allow a user to log in even if the 3rd party authentication token that was used to sign in to their account has expired. If this is set to `false`, then the token will be validated every time the user signs in to their account. This refers to the token that is stored in the `_User.authData` field. Defaults to `false`.', action: parsers.booleanParser, default: false, }, @@ -89,8 +83,7 @@ module.exports.ParseServerOptions = { }, allowOrigin: { env: 'PARSE_SERVER_ALLOW_ORIGIN', - help: - 'Sets origins for Access-Control-Allow-Origin. This can be a string for a single origin or an array of strings for multiple origins.', + help: 'Sets origins for Access-Control-Allow-Origin. This can be a string for a single origin or an array of strings for multiple origins.', action: parsers.arrayParser, }, analyticsAdapter: { @@ -109,8 +102,7 @@ module.exports.ParseServerOptions = { }, auth: { env: 'PARSE_SERVER_AUTH_PROVIDERS', - help: - 'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication', + help: 'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication', action: parsers.objectParser, }, cacheAdapter: { @@ -150,15 +142,13 @@ module.exports.ParseServerOptions = { }, convertEmailToLowercase: { env: 'PARSE_SERVER_CONVERT_EMAIL_TO_LOWERCASE', - help: - 'Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`.', + help: 'Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`.', action: parsers.booleanParser, default: false, }, convertUsernameToLowercase: { env: 'PARSE_SERVER_CONVERT_USERNAME_TO_LOWERCASE', - help: - 'Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`.', + help: 'Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`.', action: parsers.booleanParser, default: false, }, @@ -171,8 +161,7 @@ module.exports.ParseServerOptions = { }, databaseAdapter: { env: 'PARSE_SERVER_DATABASE_ADAPTER', - help: - 'Adapter module for the database; any options that are not explicitly described here are passed directly to the database client.', + help: 'Adapter module for the database; any options that are not explicitly described here are passed directly to the database client.', action: parsers.moduleOrObjectParser, }, databaseOptions: { @@ -195,8 +184,7 @@ module.exports.ParseServerOptions = { }, directAccess: { env: 'PARSE_SERVER_DIRECT_ACCESS', - help: - 'Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.

If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.

\u26A0\uFE0F In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`.', + help: 'Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.

If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.

\u26A0\uFE0F In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`.', action: parsers.booleanParser, default: true, }, @@ -211,15 +199,13 @@ module.exports.ParseServerOptions = { }, emailVerifyTokenReuseIfValid: { env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID', - help: - 'Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`.', + help: 'Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`.', action: parsers.booleanParser, default: false, }, emailVerifyTokenValidityDuration: { env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION', - help: - 'Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.
Requires option `verifyUserEmails: true`.', + help: 'Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.
Requires option `verifyUserEmails: true`.', action: parsers.numberParser('emailVerifyTokenValidityDuration'), }, enableAnonymousUsers: { @@ -230,8 +216,7 @@ module.exports.ParseServerOptions = { }, enableCollationCaseComparison: { env: 'PARSE_SERVER_ENABLE_COLLATION_CASE_COMPARISON', - help: - '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`.', + help: '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`.', action: parsers.booleanParser, default: false, }, @@ -243,15 +228,13 @@ module.exports.ParseServerOptions = { }, enableInsecureAuthAdapters: { env: 'PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS', - help: - 'Optional. Enables insecure authentication adapters. Insecure auth adapters are deprecated and will be removed in a future version. Defaults to `false`.', + help: 'Optional. Enables insecure authentication adapters. Insecure auth adapters are deprecated and will be removed in a future version. Defaults to `false`.', action: parsers.booleanParser, default: false, }, 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`.', + 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, }, @@ -267,15 +250,13 @@ module.exports.ParseServerOptions = { }, expireInactiveSessions: { env: 'PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS', - help: - 'Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.', + help: 'Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.', action: parsers.booleanParser, default: true, }, extendSessionOnUse: { env: 'PARSE_SERVER_EXTEND_SESSION_ON_USE', - help: - "Whether Parse Server should automatically extend a valid session by the sessionLength. In order to reduce the number of session updates in the database, a session will only be extended when a request is received after at least half of the current session's lifetime has passed.", + help: "Whether Parse Server should automatically extend a valid session by the sessionLength. In order to reduce the number of session updates in the database, a session will only be extended when a request is received after at least half of the current session's lifetime has passed.", action: parsers.booleanParser, default: false, }, @@ -297,8 +278,7 @@ module.exports.ParseServerOptions = { }, graphQLPath: { env: 'PARSE_SERVER_GRAPHQL_PATH', - help: - 'The mount path for the GraphQL endpoint

\u26A0\uFE0F File upload inside the GraphQL mutation system requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.

Defaults is `/graphql`.', + help: 'The mount path for the GraphQL endpoint

\u26A0\uFE0F File upload inside the GraphQL mutation system requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.

Defaults is `/graphql`.', default: '/graphql', }, graphQLPublicIntrospection: { @@ -318,8 +298,7 @@ module.exports.ParseServerOptions = { }, idempotencyOptions: { env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS', - help: - 'Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.', + help: 'Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.', action: parsers.objectParser, type: 'IdempotencyOptions', default: {}, @@ -368,14 +347,12 @@ module.exports.ParseServerOptions = { }, maintenanceKey: { env: 'PARSE_SERVER_MAINTENANCE_KEY', - help: - '(Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.

\u26A0\uFE0F This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.', + help: '(Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.

\u26A0\uFE0F This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.', required: true, }, maintenanceKeyIps: { env: 'PARSE_SERVER_MAINTENANCE_KEY_IPS', - help: - "(Optional) Restricts the use of maintenance key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the maintenance key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the maintenance key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the maintenance key.", + help: "(Optional) Restricts the use of maintenance key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the maintenance key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the maintenance key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the maintenance key.", action: parsers.arrayParser, default: ['127.0.0.1', '::1'], }, @@ -386,15 +363,13 @@ module.exports.ParseServerOptions = { }, masterKeyIps: { env: 'PARSE_SERVER_MASTER_KEY_IPS', - help: - "(Optional) Restricts the use of master key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the master key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the master key.", + help: "(Optional) Restricts the use of master key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the master key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the master key.", action: parsers.arrayParser, default: ['127.0.0.1', '::1'], }, masterKeyTtl: { env: 'PARSE_SERVER_MASTER_KEY_TTL', - help: - '(Optional) The duration in seconds for which the current `masterKey` is being used before it is requested again if `masterKey` is set to a function. If `masterKey` is not set to a function, this option has no effect. Default is `0`, which means the master key is requested by invoking the `masterKey` function every time the master key is used internally by Parse Server.', + help: '(Optional) The duration in seconds for which the current `masterKey` is being used before it is requested again if `masterKey` is set to a function. If `masterKey` is not set to a function, this option has no effect. Default is `0`, which means the master key is requested by invoking the `masterKey` function every time the master key is used internally by Parse Server.', action: parsers.numberParser('masterKeyTtl'), }, maxLimit: { @@ -404,8 +379,7 @@ module.exports.ParseServerOptions = { }, maxLogFiles: { env: 'PARSE_SERVER_MAX_LOG_FILES', - help: - "Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)", + help: "Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)", action: parsers.numberOrStringParser('maxLogFiles'), }, maxUploadSize: { @@ -472,15 +446,13 @@ module.exports.ParseServerOptions = { }, preventLoginWithUnverifiedEmail: { env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL', - help: - "Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

The `createdWith` values per scenario:
  • Password signup: `{ action: 'signup', authProvider: 'password' }`
  • Auth provider signup: `{ action: 'signup', authProvider: '' }`
  • Password login: `{ action: 'login', authProvider: 'password' }`
  • Auth provider login: function not invoked; auth provider login bypasses email verification
Default is `false`.
Requires option `verifyUserEmails: true`.", + help: "Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

The `createdWith` values per scenario:
  • Password signup: `{ action: 'signup', authProvider: 'password' }`
  • Auth provider signup: `{ action: 'signup', authProvider: '' }`
  • Password login: `{ action: 'login', authProvider: 'password' }`
  • Auth provider login: function not invoked; auth provider login bypasses email verification
Default is `false`.
Requires option `verifyUserEmails: true`.", action: parsers.booleanOrFunctionParser, default: false, }, preventSignupWithUnverifiedEmail: { env: 'PARSE_SERVER_PREVENT_SIGNUP_WITH_UNVERIFIED_EMAIL', - help: - "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`.", + help: "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`.", action: parsers.booleanParser, default: false, }, @@ -496,19 +468,16 @@ module.exports.ParseServerOptions = { }, publicServerURL: { env: 'PARSE_PUBLIC_SERVER_URL', - 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://`.', + 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', - help: - 'Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications', + help: 'Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications', action: parsers.objectParser, }, rateLimit: { env: 'PARSE_SERVER_RATE_LIMIT', - help: - "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.

\u2139\uFE0F 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.", + help: "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.

\u2139\uFE0F 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.", action: parsers.arrayParser, type: 'RateLimitOptions[]', default: [], @@ -519,13 +488,11 @@ module.exports.ParseServerOptions = { }, requestContextMiddleware: { env: 'PARSE_SERVER_REQUEST_CONTEXT_MIDDLEWARE', - help: - 'Options to customize the request context using inversion of control/dependency injection.', + help: 'Options to customize the request context using inversion of control/dependency injection.', }, requestKeywordDenylist: { env: 'PARSE_SERVER_REQUEST_KEYWORD_DENYLIST', - help: - 'An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.', + help: 'An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.', action: parsers.arrayParser, default: [ { @@ -546,8 +513,7 @@ module.exports.ParseServerOptions = { }, revokeSessionOnPasswordReset: { env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET', - help: - "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.", + help: "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.", action: parsers.booleanParser, default: true, }, @@ -572,8 +538,7 @@ module.exports.ParseServerOptions = { }, sendUserEmailVerification: { env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION', - help: - 'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.

Default is `true`.
', + help: 'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.

Default is `true`.
', action: parsers.booleanOrFunctionParser, default: true, }, @@ -583,8 +548,7 @@ module.exports.ParseServerOptions = { }, serverURL: { env: 'PARSE_SERVER_URL', - help: - 'The URL to Parse Server.

\u26A0\uFE0F Certain server features or adapters may require Parse Server to be able to call itself by making requests to the URL set in `serverURL`. If a feature requires this, it is mentioned in the documentation. In that case ensure that the URL is accessible from the server itself.', + help: 'The URL to Parse Server.

\u26A0\uFE0F Certain server features or adapters may require Parse Server to be able to call itself by making requests to the URL set in `serverURL`. If a feature requires this, it is mentioned in the documentation. In that case ensure that the URL is accessible from the server itself.', required: true, }, sessionLength: { @@ -605,15 +569,13 @@ module.exports.ParseServerOptions = { }, trustProxy: { env: 'PARSE_SERVER_TRUST_PROXY', - help: - 'The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the express trust proxy settings documentation. Defaults to `false`.', + help: 'The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the express trust proxy settings documentation. Defaults to `false`.', action: parsers.objectParser, default: [], }, userSensitiveFields: { env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS', - help: - 'Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields', + help: 'Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields', action: parsers.arrayParser, }, verbose: { @@ -623,15 +585,13 @@ module.exports.ParseServerOptions = { }, verifyServerUrl: { env: 'PARSE_SERVER_VERIFY_SERVER_URL', - help: - 'Parse Server makes a HTTP request to the URL set in `serverURL` at the end of its launch routine to verify that the launch succeeded. If this option is set to `false`, the verification will be skipped. This can be useful in environments where the server URL is not accessible from the server itself, such as when running behind a firewall or in certain containerized environments.

\u26A0\uFE0F Server URL verification requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.

Default is `true`.', + help: 'Parse Server makes a HTTP request to the URL set in `serverURL` at the end of its launch routine to verify that the launch succeeded. If this option is set to `false`, the verification will be skipped. This can be useful in environments where the server URL is not accessible from the server itself, such as when running behind a firewall or in certain containerized environments.

\u26A0\uFE0F Server URL verification requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.

Default is `true`.', action: parsers.booleanParser, default: true, }, verifyUserEmails: { env: 'PARSE_SERVER_VERIFY_USER_EMAILS', - help: - "Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

The `createdWith` values per scenario:
  • Password signup: `{ action: 'signup', authProvider: 'password' }`
  • Auth provider signup: `{ action: 'signup', authProvider: '' }`
  • Password login: `{ action: 'login', authProvider: 'password' }`
  • Auth provider login: function not invoked; auth provider login bypasses email verification
  • Resend verification email: `createdWith` is `undefined`; use the `resendRequest` property to identify those
Default is `false`.", + help: "Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

The `createdWith` values per scenario:
  • Password signup: `{ action: 'signup', authProvider: 'password' }`
  • Auth provider signup: `{ action: 'signup', authProvider: '' }`
  • Password login: `{ action: 'login', authProvider: 'password' }`
  • Auth provider login: function not invoked; auth provider login bypasses email verification
  • Resend verification email: `createdWith` is `undefined`; use the `resendRequest` property to identify those
Default is `false`.", action: parsers.booleanOrFunctionParser, default: false, }, @@ -643,65 +603,55 @@ module.exports.ParseServerOptions = { module.exports.RateLimitOptions = { errorResponseMessage: { env: 'PARSE_SERVER_RATE_LIMIT_ERROR_RESPONSE_MESSAGE', - help: - 'The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is `Too many requests.`.', + help: 'The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is `Too many requests.`.', default: 'Too many requests.', }, includeInternalRequests: { env: 'PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS', - help: - 'Optional, if `true` the rate limit will also apply to requests that are made in by Cloud Code, default is `false`. Note that a public Cloud Code function that triggers internal requests may circumvent rate limiting and be vulnerable to attacks.', + help: 'Optional, if `true` the rate limit will also apply to requests that are made in by Cloud Code, default is `false`. Note that a public Cloud Code function that triggers internal requests may circumvent rate limiting and be vulnerable to attacks.', action: parsers.booleanParser, default: false, }, includeMasterKey: { env: 'PARSE_SERVER_RATE_LIMIT_INCLUDE_MASTER_KEY', - help: - 'Optional, if `true` the rate limit will also apply to requests using the `masterKey`, default is `false`. Note that a public Cloud Code function that triggers internal requests using the `masterKey` may circumvent rate limiting and be vulnerable to attacks.', + help: 'Optional, if `true` the rate limit will also apply to requests using the `masterKey`, default is `false`. Note that a public Cloud Code function that triggers internal requests using the `masterKey` may circumvent rate limiting and be vulnerable to attacks.', action: parsers.booleanParser, default: false, }, redisUrl: { env: 'PARSE_SERVER_RATE_LIMIT_REDIS_URL', - help: - '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.', + help: '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.', }, requestCount: { env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT', - help: - 'The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied.', + help: 'The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied.', action: parsers.numberParser('requestCount'), }, requestMethods: { env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_METHODS', - help: - 'Optional, the HTTP request methods to which the rate limit should be applied, default is all methods.', + help: 'Optional, the HTTP request methods to which the rate limit should be applied, default is all methods.', action: parsers.arrayParser, }, requestPath: { env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_PATH', - help: - '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 or string patterns following path-to-regexp v8 syntax.', + help: '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 or string patterns following path-to-regexp v8 syntax.', required: true, }, requestTimeWindow: { env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_TIME_WINDOW', - help: - 'The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied.', + help: 'The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied.', action: parsers.numberParser('requestTimeWindow'), }, 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 is `ip`.', + 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 is `ip`.', default: 'ip', }, }; module.exports.SecurityOptions = { checkGroups: { env: 'PARSE_SERVER_SECURITY_CHECK_GROUPS', - help: - 'The security check groups to run. This allows to add custom security checks or override existing ones. Default are the groups defined in `CheckGroups.js`.', + help: 'The security check groups to run. This allows to add custom security checks or override existing ones. Default are the groups defined in `CheckGroups.js`.', action: parsers.arrayParser, }, enableCheck: { @@ -712,8 +662,7 @@ module.exports.SecurityOptions = { }, enableCheckLog: { env: 'PARSE_SERVER_SECURITY_ENABLE_CHECK_LOG', - help: - 'Is true if the security check report should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs.', + help: 'Is true if the security check report should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs.', action: parsers.booleanParser, default: false, }, @@ -741,28 +690,24 @@ module.exports.PagesOptions = { }, enableRouter: { env: 'PARSE_SERVER_PAGES_ENABLE_ROUTER', - help: - 'Is true if the pages router should be enabled; this is required for any of the pages options to take effect.', + help: 'Is true if the pages router should be enabled; this is required for any of the pages options to take effect.', action: parsers.booleanParser, default: false, }, forceRedirect: { env: 'PARSE_SERVER_PAGES_FORCE_REDIRECT', - help: - 'Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response).', + help: 'Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response).', action: parsers.booleanParser, default: false, }, localizationFallbackLocale: { env: 'PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE', - help: - 'The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file.', + help: 'The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file.', default: 'en', }, localizationJsonPath: { env: 'PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH', - help: - 'The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale.', + help: 'The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale.', }, pagesEndpoint: { env: 'PARSE_SERVER_PAGES_PAGES_ENDPOINT', @@ -771,13 +716,11 @@ module.exports.PagesOptions = { }, pagesPath: { env: 'PARSE_SERVER_PAGES_PAGES_PATH', - help: - "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory of the parse-server module.", + help: "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory of the parse-server module.", }, placeholders: { env: 'PARSE_SERVER_PAGES_PLACEHOLDERS', - help: - 'The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function.', + help: 'The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function.', action: parsers.objectParser, default: {}, }, @@ -904,30 +847,25 @@ module.exports.LiveQueryOptions = { module.exports.LiveQueryServerOptions = { appId: { env: 'PARSE_LIVE_QUERY_SERVER_APP_ID', - help: - 'This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.', + help: 'This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.', }, cacheTimeout: { env: 'PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT', - help: - "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).", + help: "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).", action: parsers.numberParser('cacheTimeout'), }, keyPairs: { env: 'PARSE_LIVE_QUERY_SERVER_KEY_PAIRS', - help: - 'A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.', + help: 'A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.', action: parsers.objectParser, }, logLevel: { env: 'PARSE_LIVE_QUERY_SERVER_LOG_LEVEL', - help: - 'This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.', + help: 'This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.', }, masterKey: { env: 'PARSE_LIVE_QUERY_SERVER_MASTER_KEY', - help: - 'This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.', + help: 'This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.', }, port: { env: 'PARSE_LIVE_QUERY_SERVER_PORT', @@ -951,13 +889,11 @@ module.exports.LiveQueryServerOptions = { }, serverURL: { env: 'PARSE_LIVE_QUERY_SERVER_SERVER_URL', - help: - 'This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.', + help: 'This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.', }, websocketTimeout: { env: 'PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT', - help: - 'Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).', + help: 'Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).', action: parsers.numberParser('websocketTimeout'), }, wssAdapter: { @@ -969,15 +905,13 @@ module.exports.LiveQueryServerOptions = { module.exports.IdempotencyOptions = { paths: { env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS', - help: - 'An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.', + help: 'An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.', action: parsers.arrayParser, default: [], }, ttl: { env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL', - help: - 'The duration in seconds after which a request record is discarded from the database, defaults to 300s.', + help: 'The duration in seconds after which a request record is discarded from the database, defaults to 300s.', action: parsers.numberParser('ttl'), default: 300, }, @@ -985,20 +919,17 @@ module.exports.IdempotencyOptions = { module.exports.AccountLockoutOptions = { duration: { env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION', - help: - 'Set the duration in minutes that a locked-out account remains locked out before automatically becoming unlocked.

Valid values are greater than `0` and less than `100000`.', + help: 'Set the duration in minutes that a locked-out account remains locked out before automatically becoming unlocked.

Valid values are greater than `0` and less than `100000`.', action: parsers.numberParser('duration'), }, threshold: { env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD', - help: - 'Set the number of failed sign-in attempts that will cause a user account to be locked. If the account is locked. The account will unlock after the duration set in the `duration` option has passed and no further login attempts have been made.

Valid values are greater than `0` and less than `1000`.', + help: 'Set the number of failed sign-in attempts that will cause a user account to be locked. If the account is locked. The account will unlock after the duration set in the `duration` option has passed and no further login attempts have been made.

Valid values are greater than `0` and less than `1000`.', action: parsers.numberParser('threshold'), }, unlockOnPasswordReset: { env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_UNLOCK_ON_PASSWORD_RESET', - help: - 'Set to `true` if the account should be unlocked after a successful password reset.

Default is `false`.
Requires options `duration` and `threshold` to be set.', + help: 'Set to `true` if the account should be unlocked after a successful password reset.

Default is `false`.
Requires options `duration` and `threshold` to be set.', action: parsers.booleanParser, default: false, }, @@ -1006,57 +937,48 @@ module.exports.AccountLockoutOptions = { module.exports.PasswordPolicyOptions = { doNotAllowUsername: { env: 'PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME', - help: - 'Set to `true` to disallow the username as part of the password.

Default is `false`.', + help: 'Set to `true` to disallow the username as part of the password.

Default is `false`.', action: parsers.booleanParser, default: false, }, maxPasswordAge: { env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE', - help: - 'Set the number of days after which a password expires. Login attempts fail if the user does not reset the password before expiration.', + help: 'Set the number of days after which a password expires. Login attempts fail if the user does not reset the password before expiration.', action: parsers.numberParser('maxPasswordAge'), }, maxPasswordHistory: { env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY', - help: - 'Set the number of previous password that will not be allowed to be set as new password. If the option is not set or set to `0`, no previous passwords will be considered.

Valid values are >= `0` and <= `20`.
Default is `0`.', + help: 'Set the number of previous password that will not be allowed to be set as new password. If the option is not set or set to `0`, no previous passwords will be considered.

Valid values are >= `0` and <= `20`.
Default is `0`.', action: parsers.numberParser('maxPasswordHistory'), }, resetPasswordSuccessOnInvalidEmail: { env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_PASSWORD_SUCCESS_ON_INVALID_EMAIL', - help: - 'Set to `true` if a request to reset the password should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.

Default is `true`.', + help: 'Set to `true` if a request to reset the password should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.

Default is `true`.', action: parsers.booleanParser, default: true, }, resetTokenReuseIfValid: { env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID', - help: - 'Set to `true` if a password reset token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.', + help: 'Set to `true` if a password reset token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.', action: parsers.booleanParser, default: false, }, resetTokenValidityDuration: { env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION', - help: - 'Set the validity duration of the password reset token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.', + help: 'Set the validity duration of the password reset token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.', action: parsers.numberParser('resetTokenValidityDuration'), }, validationError: { env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATION_ERROR', - help: - 'Set the error message to be sent.

Default is `Password does not meet the Password Policy requirements.`', + help: 'Set the error message to be sent.

Default is `Password does not meet the Password Policy requirements.`', }, validatorCallback: { env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_CALLBACK', - help: - 'Set a callback function to validate a password to be accepted.

If used in combination with `validatorPattern`, the password must pass both to be accepted.', + help: 'Set a callback function to validate a password to be accepted.

If used in combination with `validatorPattern`, the password must pass both to be accepted.', }, validatorPattern: { env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN', - help: - 'Set the regular expression validation pattern a password must match to be accepted.

If used in combination with `validatorCallback`, the password must pass both to be accepted.', + help: 'Set the regular expression validation pattern a password must match to be accepted.

If used in combination with `validatorCallback`, the password must pass both to be accepted.', }, }; module.exports.FileUploadOptions = { @@ -1080,8 +1002,7 @@ module.exports.FileUploadOptions = { }, fileExtensions: { env: 'PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS', - help: - "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.", + help: "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.", action: parsers.arrayParser, default: ['^(?![xXsS]?[hH][tT][mM][lL]?$)'], }, @@ -1122,147 +1043,124 @@ module.exports.LogLevel = { 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.', + 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'`.", + 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.', + 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 = { 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.', + 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: false, }, appName: { env: 'PARSE_SERVER_DATABASE_APP_NAME', - help: - 'The MongoDB driver option to specify the name of the application that created this MongoClient instance.', + 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.', + 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.', + 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.", + 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: - '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.', + help: '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.', action: parsers.booleanParser, }, autoSelectFamilyAttemptTimeout: { env: 'PARSE_SERVER_DATABASE_AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT', - help: - '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.', + help: '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'), }, clientMetadata: { env: 'PARSE_SERVER_DATABASE_CLIENT_METADATA', - help: - "Custom metadata to append to database client connections for identifying Parse Server instances in database logs. If set, this metadata will be visible in database logs during connection handshakes. This can help with debugging and monitoring in deployments with multiple database clients. Set `name` to identify your application (e.g., 'MyApp') and `version` to your application's version. Leave undefined (default) to disable this feature and avoid the additional data transfer overhead.", + help: "Custom metadata to append to database client connections for identifying Parse Server instances in database logs. If set, this metadata will be visible in database logs during connection handshakes. This can help with debugging and monitoring in deployments with multiple database clients. Set `name` to identify your application (e.g., 'MyApp') and `version` to your application's version. Leave undefined (default) to disable this feature and avoid the additional data transfer overhead.", action: parsers.objectParser, type: 'DatabaseOptionsClientMetadata', }, 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.', + 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: - '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.', + help: '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.', action: parsers.numberParser('connectTimeoutMS'), }, createIndexRoleName: { env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_ROLE_NAME', - help: - '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`.

\u26A0\uFE0F 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.', + help: '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`.

\u26A0\uFE0F 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.', action: parsers.booleanParser, default: true, }, createIndexUserEmail: { env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL', - help: - '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`.

\u26A0\uFE0F 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.', + help: '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`.

\u26A0\uFE0F 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.', action: parsers.booleanParser, default: true, }, createIndexUserEmailCaseInsensitive: { env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL_CASE_INSENSITIVE', - help: - 'Set to `true` to automatically create a case-insensitive index on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

\u26A0\uFE0F 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.', + help: 'Set to `true` to automatically create a case-insensitive index on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

\u26A0\uFE0F 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.', action: parsers.booleanParser, default: true, }, createIndexUserEmailVerifyToken: { env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL_VERIFY_TOKEN', - help: - 'Set to `true` to automatically create an index on the _email_verify_token field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

\u26A0\uFE0F 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.', + help: 'Set to `true` to automatically create an index on the _email_verify_token field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

\u26A0\uFE0F 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.', action: parsers.booleanParser, default: true, }, createIndexUserPasswordResetToken: { env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_PASSWORD_RESET_TOKEN', - help: - '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`.

\u26A0\uFE0F 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.', + help: '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`.

\u26A0\uFE0F 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.', action: parsers.booleanParser, default: true, }, createIndexUserUsername: { env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_USERNAME', - help: - '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`.

\u26A0\uFE0F 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.', + help: '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`.

\u26A0\uFE0F 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.', action: parsers.booleanParser, default: true, }, createIndexUserUsernameCaseInsensitive: { env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_USERNAME_CASE_INSENSITIVE', - help: - '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`.

\u26A0\uFE0F 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.', + help: '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`.

\u26A0\uFE0F 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.', 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.', + 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: - '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.', + help: '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.', action: parsers.booleanParser, }, enableSchemaHooks: { env: 'PARSE_SERVER_DATABASE_ENABLE_SCHEMA_HOOKS', - help: - '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.', + help: '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.', action: parsers.booleanParser, default: false, }, @@ -1273,20 +1171,17 @@ module.exports.DatabaseOptions = { }, 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.', + 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.', + 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.', + 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'), }, logClientEvents: { @@ -1297,60 +1192,50 @@ module.exports.DatabaseOptions = { }, 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.', + 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: - '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.', + 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: - 'The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver.', + help: 'The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver.', action: parsers.numberParser('maxPoolSize'), }, maxStalenessSeconds: { env: 'PARSE_SERVER_DATABASE_MAX_STALENESS_SECONDS', - help: - 'The MongoDB driver option to set the maximum replication lag for reads from secondary nodes.', + help: 'The MongoDB driver option to set the maximum replication lag for reads from secondary nodes.', action: parsers.numberParser('maxStalenessSeconds'), }, maxTimeMS: { env: 'PARSE_SERVER_DATABASE_MAX_TIME_MS', - help: - 'The MongoDB driver option to set a cumulative time limit in milliseconds for processing operations on a cursor.', + help: 'The MongoDB driver option to set a cumulative time limit in milliseconds for processing operations on a cursor.', action: parsers.numberParser('maxTimeMS'), }, minPoolSize: { env: 'PARSE_SERVER_DATABASE_MIN_POOL_SIZE', - help: - 'The MongoDB driver option to set the minimum number of opened, cached, ready-to-use database connections maintained by the driver.', + help: '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.', + 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.', + 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.', + 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.', + 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', @@ -1362,14 +1247,12 @@ module.exports.DatabaseOptions = { }, 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.', + 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.', + 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', @@ -1383,31 +1266,26 @@ module.exports.DatabaseOptions = { }, schemaCacheTtl: { env: 'PARSE_SERVER_DATABASE_SCHEMA_CACHE_TTL', - help: - '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.', + help: '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.', + 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: - 'The MongoDB driver option to specify the amount of time in milliseconds for a server to be considered suitable for selection.', + 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: - '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.', + help: '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.', + 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: { @@ -1416,8 +1294,7 @@ module.exports.DatabaseOptions = { }, ssl: { env: 'PARSE_SERVER_DATABASE_SSL', - help: - 'The MongoDB driver option to enable or disable TLS/SSL for the connection (equivalent to tls option).', + help: 'The MongoDB driver option to enable or disable TLS/SSL for the connection (equivalent to tls option).', action: parsers.booleanParser, }, tls: { @@ -1427,25 +1304,21 @@ module.exports.DatabaseOptions = { }, 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.', + 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.', + 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.', + 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.", + 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', @@ -1458,14 +1331,12 @@ module.exports.DatabaseOptions = { }, 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.', + 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).', + help: 'The MongoDB driver option to specify the compression level if using zlib for network compression (0-9).', action: parsers.numberParser('zlibCompressionLevel'), }, }; @@ -1491,38 +1362,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`. See [LogLevel](LogLevel.html) for available values.', + 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`. See [LogLevel](LogLevel.html) for available values.', + help: 'Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.', default: 'info', }, signupUsernameTaken: { env: 'PARSE_SERVER_LOG_LEVELS_SIGNUP_USERNAME_TAKEN', - help: - 'Log level used when a sign-up fails because the username already exists. Default is `info`. See [LogLevel](LogLevel.html) for available values.', + help: 'Log level used when a sign-up fails because the username already exists. 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`. See [LogLevel](LogLevel.html) for available values.', + help: '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`. See [LogLevel](LogLevel.html) for available values.', + help: '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`. See [LogLevel](LogLevel.html) for available values.', + help: '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', }, }; From 4c9c9489f062bec6d751b23f4a68aea2a63936bd Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 7 Feb 2026 17:03:39 +0000 Subject: [PATCH 13/36] feat: Add `Parse.File.url` validation with config `fileUpload.allowedFileUrlDomains` against SSRF attacks (#10044) --- README.md | 28 +++ spec/Deprecator.spec.js | 33 ++++ spec/FileUrlValidator.spec.js | 141 ++++++++++++++ spec/ParseFile.spec.js | 253 ++++++++++++++++++++++++++ spec/ParseGraphQLServer.spec.js | 46 +++++ src/Config.js | 11 ++ src/Controllers/DatabaseController.js | 12 ++ src/Deprecator/Deprecations.js | 8 +- src/FileUrlValidator.js | 68 +++++++ src/GraphQL/transformers/mutation.js | 4 + src/Options/Definitions.js | 6 + src/Options/docs.js | 1 + src/Options/index.js | 3 + src/ParseServer.ts | 2 +- src/Routers/FunctionsRouter.js | 4 + types/Options/index.d.ts | 1 + 16 files changed, 619 insertions(+), 2 deletions(-) create mode 100644 spec/FileUrlValidator.spec.js create mode 100644 src/FileUrlValidator.js diff --git a/README.md b/README.md index de0b21d442..e0a10a79d2 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ A big _thank you_ 🙏 to our [sponsors](#sponsors) and [backers](#backers) who - [Using Environment Variables](#using-environment-variables) - [Available Adapters](#available-adapters) - [Configuring File Adapters](#configuring-file-adapters) + - [Restricting File URL Domains](#restricting-file-url-domains) - [Idempotency Enforcement](#idempotency-enforcement) - [Localization](#localization) - [Pages](#pages) @@ -491,6 +492,33 @@ Parse Server allows developers to choose from several options when hosting files `GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using Amazon S3, Google Cloud Storage, or local file storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters). +### Restricting File URL Domains + +Parse objects can reference files by URL. To prevent [SSRF attacks](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery) via crafted file URLs, you can restrict the allowed URL domains using the `fileUpload.allowedFileUrlDomains` option. + +This protects against scenarios where an attacker provides a `Parse.File` with an arbitrary URL, for example as a Cloud Function parameter or in a field of type `Object` or `Array`. If Cloud Code or a client calls `getData()` on such a file, the Parse SDK makes an HTTP request to that URL, potentially leaking the server or client IP address and accessing internal services. + +> [!NOTE] +> Fields of type `Parse.File` in the Parse schema are not affected by this attack, because Parse Server discards the URL on write and dynamically generates it on read based on the file adapter configuration. + +```javascript +const parseServer = new ParseServer({ + ...otherOptions, + fileUpload: { + allowedFileUrlDomains: ['cdn.example.com', '*.example.com'], + }, +}); +``` + +| Parameter | Optional | Type | Default | Environment Variable | +|---|---|---|---|---| +| `fileUpload.allowedFileUrlDomains` | yes | `String[]` | `['*']` | `PARSE_SERVER_FILE_UPLOAD_ALLOWED_FILE_URL_DOMAINS` | + +- `['*']` (default) allows file URLs with any domain. +- `['cdn.example.com']` allows only exact hostname matches. +- `['*.example.com']` allows any subdomain of `example.com`. +- `[]` blocks all file URLs; only files referenced by name are allowed. + ## Idempotency Enforcement **Caution, this is an experimental feature that may not be appropriate for production.** diff --git a/spec/Deprecator.spec.js b/spec/Deprecator.spec.js index f5b6812699..210a503fb8 100644 --- a/spec/Deprecator.spec.js +++ b/spec/Deprecator.spec.js @@ -70,4 +70,37 @@ describe('Deprecator', () => { Deprecator.scanParseServerOptions({ databaseOptions: { testOption: true } }); expect(logSpy).not.toHaveBeenCalled(); }); + + it('logs deprecation for allowedFileUrlDomains when not set', async () => { + const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {}); + + // Pass a fresh fileUpload object without allowedFileUrlDomains to avoid + // inheriting the mutated default from a previous reconfigureServer() call. + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + enableForAnonymousUser: true, + enableForAuthenticatedUser: true, + }, + }); + expect(logSpy).toHaveBeenCalledWith( + jasmine.objectContaining({ + optionKey: 'fileUpload.allowedFileUrlDomains', + changeNewDefault: '[]', + }) + ); + }); + + it('does not log deprecation for allowedFileUrlDomains when explicitly set', async () => { + const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {}); + + await reconfigureServer({ + fileUpload: { allowedFileUrlDomains: ['*'] }, + }); + expect(logSpy).not.toHaveBeenCalledWith( + jasmine.objectContaining({ + optionKey: 'fileUpload.allowedFileUrlDomains', + }) + ); + }); }); diff --git a/spec/FileUrlValidator.spec.js b/spec/FileUrlValidator.spec.js new file mode 100644 index 0000000000..886aaf75e3 --- /dev/null +++ b/spec/FileUrlValidator.spec.js @@ -0,0 +1,141 @@ +'use strict'; + +const { validateFileUrl, validateFileUrlsInObject } = require('../src/FileUrlValidator'); + +describe('FileUrlValidator', () => { + describe('validateFileUrl', () => { + it('allows null, undefined, and empty string URLs', () => { + const config = { fileUpload: { allowedFileUrlDomains: [] } }; + expect(() => validateFileUrl(null, config)).not.toThrow(); + expect(() => validateFileUrl(undefined, config)).not.toThrow(); + expect(() => validateFileUrl('', config)).not.toThrow(); + }); + + it('allows any URL when allowedFileUrlDomains contains wildcard', () => { + const config = { fileUpload: { allowedFileUrlDomains: ['*'] } }; + expect(() => validateFileUrl('http://malicious.example.com/file.txt', config)).not.toThrow(); + expect(() => validateFileUrl('http://malicious.example.com/leak', config)).not.toThrow(); + }); + + it('allows any URL when allowedFileUrlDomains is not an array', () => { + expect(() => validateFileUrl('http://example.com/file', {})).not.toThrow(); + expect(() => validateFileUrl('http://example.com/file', { fileUpload: {} })).not.toThrow(); + expect(() => validateFileUrl('http://example.com/file', null)).not.toThrow(); + }); + + it('rejects all URLs when allowedFileUrlDomains is empty', () => { + const config = { fileUpload: { allowedFileUrlDomains: [] } }; + expect(() => validateFileUrl('http://example.com/file', config)).toThrowError( + /not allowed/ + ); + }); + + it('allows URLs matching exact hostname', () => { + const config = { fileUpload: { allowedFileUrlDomains: ['cdn.example.com'] } }; + expect(() => validateFileUrl('https://cdn.example.com/files/test.txt', config)).not.toThrow(); + }); + + it('rejects URLs not matching any allowed hostname', () => { + const config = { fileUpload: { allowedFileUrlDomains: ['cdn.example.com'] } }; + expect(() => validateFileUrl('http://malicious.example.com/file', config)).toThrowError( + /not allowed/ + ); + }); + + it('supports wildcard subdomain matching', () => { + const config = { fileUpload: { allowedFileUrlDomains: ['*.example.com'] } }; + expect(() => validateFileUrl('https://cdn.example.com/file.txt', config)).not.toThrow(); + expect(() => validateFileUrl('https://us-east.cdn.example.com/file.txt', config)).not.toThrow(); + expect(() => validateFileUrl('https://example.net/file.txt', config)).toThrowError( + /not allowed/ + ); + }); + + it('performs case-insensitive hostname matching', () => { + const config = { fileUpload: { allowedFileUrlDomains: ['CDN.Example.COM'] } }; + expect(() => validateFileUrl('https://cdn.example.com/file.txt', config)).not.toThrow(); + }); + + it('throws on invalid URL strings', () => { + const config = { fileUpload: { allowedFileUrlDomains: ['example.com'] } }; + expect(() => validateFileUrl('not-a-url', config)).toThrowError( + /Invalid file URL/ + ); + }); + + it('supports multiple allowed domains', () => { + const config = { fileUpload: { allowedFileUrlDomains: ['cdn1.example.com', 'cdn2.example.com'] } }; + expect(() => validateFileUrl('https://cdn1.example.com/file.txt', config)).not.toThrow(); + expect(() => validateFileUrl('https://cdn2.example.com/file.txt', config)).not.toThrow(); + expect(() => validateFileUrl('https://cdn3.example.com/file.txt', config)).toThrowError( + /not allowed/ + ); + }); + + it('does not allow partial hostname matches', () => { + const config = { fileUpload: { allowedFileUrlDomains: ['example.com'] } }; + expect(() => validateFileUrl('https://notexample.com/file.txt', config)).toThrowError( + /not allowed/ + ); + expect(() => validateFileUrl('https://example.com.malicious.example.com/file.txt', config)).toThrowError( + /not allowed/ + ); + }); + }); + + describe('validateFileUrlsInObject', () => { + const config = { fileUpload: { allowedFileUrlDomains: ['example.com'] } }; + + it('validates file URLs in flat objects', () => { + expect(() => + validateFileUrlsInObject( + { file: { __type: 'File', name: 'test.txt', url: 'http://malicious.example.com/file' } }, + config + ) + ).toThrowError(/not allowed/); + }); + + it('validates file URLs in nested objects', () => { + expect(() => + validateFileUrlsInObject( + { nested: { deep: { file: { __type: 'File', name: 'test.txt', url: 'http://malicious.example.com/file' } } } }, + config + ) + ).toThrowError(/not allowed/); + }); + + it('validates file URLs in arrays', () => { + expect(() => + validateFileUrlsInObject( + [{ __type: 'File', name: 'test.txt', url: 'http://malicious.example.com/file' }], + config + ) + ).toThrowError(/not allowed/); + }); + + it('allows files without URLs', () => { + expect(() => + validateFileUrlsInObject( + { file: { __type: 'File', name: 'test.txt' } }, + config + ) + ).not.toThrow(); + }); + + it('allows files with permitted URLs', () => { + expect(() => + validateFileUrlsInObject( + { file: { __type: 'File', name: 'test.txt', url: 'http://example.com/file.txt' } }, + config + ) + ).not.toThrow(); + }); + + it('handles null, undefined, and primitive values', () => { + expect(() => validateFileUrlsInObject(null, config)).not.toThrow(); + expect(() => validateFileUrlsInObject(undefined, config)).not.toThrow(); + expect(() => validateFileUrlsInObject('string', config)).not.toThrow(); + expect(() => validateFileUrlsInObject(42, config)).not.toThrow(); + }); + }); +}); diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 5c1c3c99e7..46496a0768 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -1368,6 +1368,34 @@ describe('Parse.File testing', () => { }, }) ).toBeRejectedWith('fileUpload.fileExtensions must be an array.'); + await expectAsync( + reconfigureServer({ + fileUpload: { + allowedFileUrlDomains: 'not-an-array', + }, + }) + ).toBeRejectedWith('fileUpload.allowedFileUrlDomains must be an array.'); + await expectAsync( + reconfigureServer({ + fileUpload: { + allowedFileUrlDomains: [123], + }, + }) + ).toBeRejectedWith('fileUpload.allowedFileUrlDomains must contain only non-empty strings.'); + await expectAsync( + reconfigureServer({ + fileUpload: { + allowedFileUrlDomains: [''], + }, + }) + ).toBeRejectedWith('fileUpload.allowedFileUrlDomains must contain only non-empty strings.'); + await expectAsync( + reconfigureServer({ + fileUpload: { + allowedFileUrlDomains: ['example.com'], + }, + }) + ).toBeResolved(); }); }); @@ -1625,4 +1653,229 @@ describe('Parse.File testing', () => { expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/); }); }); + + describe('File URL domain validation for SSRF prevention', () => { + it('rejects cloud function call with disallowed file URL', async () => { + await reconfigureServer({ + fileUpload: { + allowedFileUrlDomains: [], + }, + }); + + Parse.Cloud.define('setUserIcon', () => {}); + + await expectAsync( + Parse.Cloud.run('setUserIcon', { + file: { __type: 'File', name: 'file.txt', url: 'http://malicious.example.com/leak' }, + }) + ).toBeRejectedWith( + jasmine.objectContaining({ message: jasmine.stringMatching(/not allowed/) }) + ); + }); + + it('rejects REST API create with disallowed file URL', async () => { + await reconfigureServer({ + fileUpload: { + allowedFileUrlDomains: [], + }, + }); + + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: { + file: { + __type: 'File', + name: 'test.txt', + url: 'http://malicious.example.com/file', + }, + }, + }) + ).toBeRejectedWith(jasmine.objectContaining({ status: 400 })); + }); + + it('rejects REST API update with disallowed file URL', async () => { + const obj = new Parse.Object('TestObject'); + await obj.save(); + + await reconfigureServer({ + fileUpload: { + allowedFileUrlDomains: [], + }, + }); + + await expectAsync( + request({ + method: 'PUT', + url: `http://localhost:8378/1/classes/TestObject/${obj.id}`, + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: { + file: { + __type: 'File', + name: 'test.txt', + url: 'http://malicious.example.com/file', + }, + }, + }) + ).toBeRejectedWith(jasmine.objectContaining({ status: 400 })); + }); + + it('allows file URLs matching configured domains', async () => { + await reconfigureServer({ + fileUpload: { + allowedFileUrlDomains: ['cdn.example.com'], + }, + }); + + Parse.Cloud.define('setUserIcon', () => 'ok'); + + const result = await Parse.Cloud.run('setUserIcon', { + file: { __type: 'File', name: 'file.txt', url: 'http://cdn.example.com/file.txt' }, + }); + expect(result).toBe('ok'); + }); + + it('allows file URLs when default wildcard is used', async () => { + Parse.Cloud.define('setUserIcon', () => 'ok'); + + const result = await Parse.Cloud.run('setUserIcon', { + file: { __type: 'File', name: 'file.txt', url: 'http://example.com/file.txt' }, + }); + expect(result).toBe('ok'); + }); + + it('allows files with server-hosted URLs even when domains are restricted', async () => { + const file = new Parse.File('test.txt', [1, 2, 3]); + await file.save(); + + await reconfigureServer({ + fileUpload: { + allowedFileUrlDomains: ['localhost'], + }, + }); + + const result = await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: { + file: { + __type: 'File', + name: file.name(), + url: file.url(), + }, + }, + }); + expect(result.status).toBe(201); + }); + + it('allows REST API create with file URL when default wildcard is used', async () => { + const result = await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: { + file: { + __type: 'File', + name: 'test.txt', + url: 'http://example.com/file.txt', + }, + }, + }); + expect(result.status).toBe(201); + }); + + it('allows cloud function with name-only file when domains are restricted', async () => { + await reconfigureServer({ + fileUpload: { + allowedFileUrlDomains: [], + }, + }); + + Parse.Cloud.define('processFile', req => req.params.file.name()); + + const result = await Parse.Cloud.run('processFile', { + file: { __type: 'File', name: 'test.txt' }, + }); + expect(result).toBe('test.txt'); + }); + + it('rejects disallowed file URL in array field', async () => { + await reconfigureServer({ + fileUpload: { + allowedFileUrlDomains: [], + }, + }); + + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: { + files: [ + { + __type: 'File', + name: 'test.txt', + url: 'http://malicious.example.com/file', + }, + ], + }, + }) + ).toBeRejectedWith(jasmine.objectContaining({ status: 400 })); + }); + + it('rejects disallowed file URL nested in object', async () => { + await reconfigureServer({ + fileUpload: { + allowedFileUrlDomains: [], + }, + }); + + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: { + data: { + nested: { + file: { + __type: 'File', + name: 'test.txt', + url: 'http://malicious.example.com/file', + }, + }, + }, + }, + }) + ).toBeRejectedWith(jasmine.objectContaining({ status: 400 })); + }); + }); }); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 3477f9d01c..40df9ea1e0 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -10240,6 +10240,52 @@ describe('ParseGraphQLServer', () => { } }); + it('should reject file with disallowed URL domain', async () => { + try { + parseServer = await global.reconfigureServer({ + publicServerURL: 'http://localhost:13377/parse', + fileUpload: { + allowedFileUrlDomains: [], + }, + }); + await createGQLFromParseServer(parseServer); + + const schemaController = await parseServer.config.databaseController.loadSchema(); + await schemaController.addClassIfNotExists('SomeClass', { + someField: { type: 'File' }, + }); + await resetGraphQLCache(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: CreateSomeClassFieldsInput) { + createSomeClass(input: { fields: $fields }) { + someClass { + id + } + } + } + `, + variables: { + fields: { + someField: { + file: { + name: 'test.txt', + url: 'http://malicious.example.com/leak', + __type: 'File', + }, + }, + }, + }, + }); + fail('should have thrown'); + expect(createResult).toBeUndefined(); + } catch (e) { + expect(e.message).toMatch(/not allowed/); + } + }); + it('should support files on required file', async () => { try { parseServer = await global.reconfigureServer({ diff --git a/src/Config.js b/src/Config.js index 9b8d31a58b..8af67cd543 100644 --- a/src/Config.js +++ b/src/Config.js @@ -550,6 +550,17 @@ export class Config { } else if (!Array.isArray(fileUpload.fileExtensions)) { throw 'fileUpload.fileExtensions must be an array.'; } + if (fileUpload.allowedFileUrlDomains === undefined) { + fileUpload.allowedFileUrlDomains = FileUploadOptions.allowedFileUrlDomains.default; + } else if (!Array.isArray(fileUpload.allowedFileUrlDomains)) { + throw 'fileUpload.allowedFileUrlDomains must be an array.'; + } else { + for (const domain of fileUpload.allowedFileUrlDomains) { + if (typeof domain !== 'string' || domain === '') { + throw 'fileUpload.allowedFileUrlDomains must contain only non-empty strings.'; + } + } + } } static validateIps(field, masterKeyIps) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index f08bface5a..c2da329358 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -499,6 +499,12 @@ class DatabaseController { } catch (error) { return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error)); } + try { + const { validateFileUrlsInObject } = require('../FileUrlValidator'); + validateFileUrlsInObject(update, this.options); + } catch (error) { + return Promise.reject(error instanceof Parse.Error ? error : new Parse.Error(Parse.Error.FILE_SAVE_ERROR, error.message || error)); + } const originalQuery = query; const originalUpdate = update; // Make a copy of the object, so we don't mutate the incoming data. @@ -836,6 +842,12 @@ class DatabaseController { } catch (error) { return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error)); } + try { + const { validateFileUrlsInObject } = require('../FileUrlValidator'); + validateFileUrlsInObject(object, this.options); + } catch (error) { + return Promise.reject(error instanceof Parse.Error ? error : new Parse.Error(Parse.Error.FILE_SAVE_ERROR, error.message || error)); + } // Make a copy of the object, so we don't mutate the incoming data. const originalObject = object; object = transformObjectACL(object); diff --git a/src/Deprecator/Deprecations.js b/src/Deprecator/Deprecations.js index cd47d80c4e..0b04902500 100644 --- a/src/Deprecator/Deprecations.js +++ b/src/Deprecator/Deprecations.js @@ -15,4 +15,10 @@ * * If there are no deprecations, this must return an empty array. */ -module.exports = []; +module.exports = [ + { + optionKey: 'fileUpload.allowedFileUrlDomains', + changeNewDefault: '[]', + solution: "Set 'fileUpload.allowedFileUrlDomains' to the domains you want to allow, or to '[]' to block all file URLs.", + }, +]; diff --git a/src/FileUrlValidator.js b/src/FileUrlValidator.js new file mode 100644 index 0000000000..6554fea51c --- /dev/null +++ b/src/FileUrlValidator.js @@ -0,0 +1,68 @@ +const Parse = require('parse/node').Parse; + +/** + * Validates whether a File URL is allowed based on the configured allowed domains. + * @param {string} fileUrl - The URL to validate. + * @param {Object} config - The Parse Server config object. + * @throws {Parse.Error} If the URL is not allowed. + */ +function validateFileUrl(fileUrl, config) { + if (fileUrl == null || fileUrl === '') { + return; + } + + const domains = config?.fileUpload?.allowedFileUrlDomains; + if (!Array.isArray(domains) || domains.includes('*')) { + return; + } + + let parsedUrl; + try { + parsedUrl = new URL(fileUrl); + } catch { + throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `Invalid file URL.`); + } + + const fileHostname = parsedUrl.hostname.toLowerCase(); + for (const domain of domains) { + const d = domain.toLowerCase(); + if (fileHostname === d) { + return; + } + if (d.startsWith('*.') && fileHostname.endsWith(d.slice(1))) { + return; + } + } + + throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File URL domain '${parsedUrl.hostname}' is not allowed.`); +} + +/** + * Recursively scans an object for File type fields and validates their URLs. + * @param {any} obj - The object to scan. + * @param {Object} config - The Parse Server config object. + * @throws {Parse.Error} If any File URL is not allowed. + */ +function validateFileUrlsInObject(obj, config) { + if (obj == null || typeof obj !== 'object') { + return; + } + if (Array.isArray(obj)) { + for (const item of obj) { + validateFileUrlsInObject(item, config); + } + return; + } + if (obj.__type === 'File' && obj.url) { + validateFileUrl(obj.url, config); + return; + } + for (const key of Object.keys(obj)) { + const value = obj[key]; + if (value && typeof value === 'object') { + validateFileUrlsInObject(value, config); + } + } +} + +module.exports = { validateFileUrl, validateFileUrlsInObject }; diff --git a/src/GraphQL/transformers/mutation.js b/src/GraphQL/transformers/mutation.js index 833ec93294..a879dcbdc2 100644 --- a/src/GraphQL/transformers/mutation.js +++ b/src/GraphQL/transformers/mutation.js @@ -97,6 +97,10 @@ const transformers = { const { fileInfo } = await handleUpload(upload, config); return { ...fileInfo, __type: 'File' }; } else if (file && file.name) { + if (file.url) { + const { validateFileUrl } = require('../../FileUrlValidator'); + validateFileUrl(file.url, config); + } return { name: file.name, __type: 'File', url: file.url }; } throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.'); diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 61ce28caec..7a3c5043df 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -982,6 +982,12 @@ module.exports.PasswordPolicyOptions = { }, }; module.exports.FileUploadOptions = { + allowedFileUrlDomains: { + env: 'PARSE_SERVER_FILE_UPLOAD_ALLOWED_FILE_URL_DOMAINS', + help: "Sets the allowed hostnames for file URLs referenced in Parse objects. When a File object includes a URL, its hostname must match one of these entries to be accepted. Supports exact hostnames (e.g., `'cdn.example.com'`) and wildcard subdomains (e.g., `'*.example.com'`). Use `['*']` to allow any domain. Use `[]` to block all file URLs (only name-based files allowed).", + action: parsers.arrayParser, + default: ['*'], + }, enableForAnonymousUser: { env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_ANONYMOUS_USER', help: 'Is true if file upload should be allowed for anonymous users.', diff --git a/src/Options/docs.js b/src/Options/docs.js index 1f63cdda2e..bbe4bf56ed 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -232,6 +232,7 @@ /** * @interface FileUploadOptions + * @property {String[]} allowedFileUrlDomains Sets the allowed hostnames for file URLs referenced in Parse objects. When a File object includes a URL, its hostname must match one of these entries to be accepted. Supports exact hostnames (e.g., `'cdn.example.com'`) and wildcard subdomains (e.g., `'*.example.com'`). Use `['*']` to allow any domain. Use `[]` to block all file URLs (only name-based files allowed). * @property {Boolean} enableForAnonymousUser Is true if file upload should be allowed for anonymous users. * @property {Boolean} enableForAuthenticatedUser Is true if file upload should be allowed for authenticated users. * @property {Boolean} enableForPublic Is true if file upload should be allowed for anyone, regardless of user authentication. diff --git a/src/Options/index.js b/src/Options/index.js index fd2b00e3c6..263154f9d6 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -630,6 +630,9 @@ export interface FileUploadOptions { /* Is true if file upload should be allowed for anyone, regardless of user authentication. :DEFAULT: false */ enableForPublic: ?boolean; + /* Sets the allowed hostnames for file URLs referenced in Parse objects. When a File object includes a URL, its hostname must match one of these entries to be accepted. Supports exact hostnames (e.g., `'cdn.example.com'`) and wildcard subdomains (e.g., `'*.example.com'`). Use `['*']` to allow any domain. Use `[]` to block all file URLs (only name-based files allowed). + :DEFAULT: ["*"] */ + allowedFileUrlDomains: ?(string[]); } /* 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) */ diff --git a/src/ParseServer.ts b/src/ParseServer.ts index fea0764cf5..1e916efe61 100644 --- a/src/ParseServer.ts +++ b/src/ParseServer.ts @@ -532,7 +532,7 @@ class ParseServer { let url; try { url = new URL(string); - } catch (_) { + } catch { return false; } return url.protocol === 'http:' || url.protocol === 'https:'; diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 93183f6f76..f116cdc9a8 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -17,6 +17,10 @@ function parseObject(obj, config) { } else if (obj && obj.__type == 'Date') { return Object.assign(new Date(obj.iso), obj); } else if (obj && obj.__type == 'File') { + if (obj.url) { + const { validateFileUrl } = require('../FileUrlValidator'); + validateFileUrl(obj.url, config); + } return Parse.File.fromJSON(obj); } else if (obj && obj.__type == 'Pointer') { return Parse.Object.fromJSON({ diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index e0c1bbc1ec..4b2e75fb2c 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -236,6 +236,7 @@ export interface PasswordPolicyOptions { resetPasswordSuccessOnInvalidEmail?: boolean; } export interface FileUploadOptions { + allowedFileUrlDomains?: string[]; fileExtensions?: (string[]); enableForAnonymousUser?: boolean; enableForAuthenticatedUser?: boolean; From 96b8c627d7a5e14b9e432b19d94faf863e3fb159 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 7 Feb 2026 17:04:27 +0000 Subject: [PATCH 14/36] chore(release): 9.3.0-alpha.3 [skip ci] # [9.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.2...9.3.0-alpha.3) (2026-02-07) ### Features * Add `Parse.File.url` validation with config `fileUpload.allowedFileUrlDomains` against SSRF attacks ([#10044](https://github.com/parse-community/parse-server/issues/10044)) ([4c9c948](https://github.com/parse-community/parse-server/commit/4c9c9489f062bec6d751b23f4a68aea2a63936bd)) --- 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 91f949a1e8..4ce144e163 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [9.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.2...9.3.0-alpha.3) (2026-02-07) + + +### Features + +* Add `Parse.File.url` validation with config `fileUpload.allowedFileUrlDomains` against SSRF attacks ([#10044](https://github.com/parse-community/parse-server/issues/10044)) ([4c9c948](https://github.com/parse-community/parse-server/commit/4c9c9489f062bec6d751b23f4a68aea2a63936bd)) + # [9.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.1...9.3.0-alpha.2) (2026-02-06) diff --git a/package-lock.json b/package-lock.json index 32a39338cb..279b1dfe49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.3.0-alpha.2", + "version": "9.3.0-alpha.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.3.0-alpha.2", + "version": "9.3.0-alpha.3", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 37ffc08cf9..faf11bfdc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.3.0-alpha.2", + "version": "9.3.0-alpha.3", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From d186471d45fb7dcb0a110c8e1a2d64a2f45c8442 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:00:48 +0000 Subject: [PATCH 15/36] refactor: Bump eslint-plugin-unused-imports from 4.3.0 to 4.4.1 (#10048) --- package-lock.json | 17 ++++++++--------- package.json | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 279b1dfe49..c688a3c228 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,7 +81,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.4.1", "form-data": "4.0.5", "globals": "16.2.0", "graphql-tag": "2.12.6", @@ -9916,14 +9916,13 @@ } }, "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==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", + "integrity": "sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==", "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" + "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@typescript-eslint/eslint-plugin": { @@ -29458,9 +29457,9 @@ } }, "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==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", + "integrity": "sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json index faf11bfdc5..e2ba512c2a 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,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.4.1", "form-data": "4.0.5", "globals": "16.2.0", "graphql-tag": "2.12.6", From 87284a839a2f508b932757d4a9f376287460af89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:22:19 +0000 Subject: [PATCH 16/36] refactor: Bump express-rate-limit from 7.5.1 to 8.2.1 (#10046) --- package-lock.json | 35 ++++++++++++++++++++++++++--------- package.json | 2 +- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index c688a3c228..26cece2d25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "cors": "2.8.6", "deepcopy": "2.1.0", "express": "5.2.1", - "express-rate-limit": "7.5.1", + "express-rate-limit": "8.2.1", "follow-redirects": "1.15.9", "graphql": "16.11.0", "graphql-list-fields": "2.0.4", @@ -10341,10 +10341,12 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "license": "MIT", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "dependencies": { + "ip-address": "10.0.1" + }, "engines": { "node": ">= 16" }, @@ -12273,6 +12275,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -29679,10 +29689,12 @@ } }, "express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "requires": {} + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "requires": { + "ip-address": "10.0.1" + } }, "extend": { "version": "3.0.2", @@ -30997,6 +31009,11 @@ "p-is-promise": "^3.0.0" } }, + "ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/package.json b/package.json index e2ba512c2a..b9ad028401 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "cors": "2.8.6", "deepcopy": "2.1.0", "express": "5.2.1", - "express-rate-limit": "7.5.1", + "express-rate-limit": "8.2.1", "follow-redirects": "1.15.9", "graphql": "16.11.0", "graphql-list-fields": "2.0.4", From 79f581b97e1ba587ddd341e17dbf1e128db1701f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:23:36 +0000 Subject: [PATCH 17/36] refactor: Bump globals from 16.2.0 to 17.3.0 (#10049) --- package-lock.json | 15 +++++++-------- package.json | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 26cece2d25..92bb2946c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,7 +83,7 @@ "eslint-plugin-expect-type": "0.6.2", "eslint-plugin-unused-imports": "4.4.1", "form-data": "4.0.5", - "globals": "16.2.0", + "globals": "17.3.0", "graphql-tag": "2.12.6", "jasmine": "5.7.1", "jasmine-spec-reporter": "7.0.0", @@ -11464,11 +11464,10 @@ } }, "node_modules/globals": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -30450,9 +30449,9 @@ } }, "globals": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", "dev": true }, "globby": { diff --git a/package.json b/package.json index b9ad028401..dfa6097b34 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "eslint-plugin-expect-type": "0.6.2", "eslint-plugin-unused-imports": "4.4.1", "form-data": "4.0.5", - "globals": "16.2.0", + "globals": "17.3.0", "graphql-tag": "2.12.6", "jasmine": "5.7.1", "jasmine-spec-reporter": "7.0.0", From e64b52f77c8c66d8d8514a11ae22f370f98c2464 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:44:46 +0000 Subject: [PATCH 18/36] refactor: Bump @actions/core from 1.11.1 to 3.0.0 (#10047) --- benchmark/performance.js | 3 +- ci/CiVersionCheck.js | 2 +- ci/definitionsCheck.js | 2 +- ci/nodeEngineCheck.js | 3 +- package-lock.json | 83 ++++++++++++++++++++++++---------------- package.json | 2 +- 6 files changed, 57 insertions(+), 38 deletions(-) diff --git a/benchmark/performance.js b/benchmark/performance.js index c37ac09777..b373a6a7a8 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -8,7 +8,6 @@ * Run with: npm run benchmark */ -const core = require('@actions/core'); const Parse = require('parse/node'); const { performance } = require('node:perf_hooks'); const { MongoClient } = require('mongodb'); @@ -25,6 +24,7 @@ const LOG_ITERATIONS = false; // Parse Server instance let parseServer; let mongoClient; +let core; // Logging helpers const logInfo = message => core.info(message); @@ -529,6 +529,7 @@ async function benchmarkQueryWithIncludeNested(name) { * Run all benchmarks */ async function runBenchmarks() { + core = await import('@actions/core'); logInfo('Starting Parse Server Performance Benchmarks...'); let server; diff --git a/ci/CiVersionCheck.js b/ci/CiVersionCheck.js index b06620b246..20986a0b15 100644 --- a/ci/CiVersionCheck.js +++ b/ci/CiVersionCheck.js @@ -1,4 +1,3 @@ -const core = require('@actions/core'); const semver = require('semver'); const yaml = require('yaml'); const fs = require('fs').promises; @@ -220,6 +219,7 @@ class CiVersionCheck { * Runs the check. */ async check() { + const core = await import('@actions/core'); /* eslint-disable no-console */ try { console.log(`\nChecking ${this.packageName} versions in CI environments...`); diff --git a/ci/definitionsCheck.js b/ci/definitionsCheck.js index 476dad8d0e..b4b9e88d0a 100644 --- a/ci/definitionsCheck.js +++ b/ci/definitionsCheck.js @@ -1,8 +1,8 @@ const fs = require('fs').promises; const { exec } = require('child_process'); -const core = require('@actions/core'); const util = require('util'); (async () => { + const core = await import('@actions/core'); const [currentDefinitions, currentDocs] = await Promise.all([ fs.readFile('./src/Options/Definitions.js', 'utf8'), fs.readFile('./src/Options/docs.js', 'utf8'), diff --git a/ci/nodeEngineCheck.js b/ci/nodeEngineCheck.js index 65a806f760..e2c4553604 100644 --- a/ci/nodeEngineCheck.js +++ b/ci/nodeEngineCheck.js @@ -1,7 +1,7 @@ -const core = require('@actions/core'); const semver = require('semver'); const fs = require('fs').promises; const path = require('path'); +let core; /** * This checks whether any package dependency requires a minimum node engine @@ -137,6 +137,7 @@ class NodeEngineCheck { } async function check() { + core = await import('@actions/core'); // Define paths const nodeModulesPath = path.join(__dirname, '../node_modules'); const packageJsonPath = path.join(__dirname, '../package.json'); diff --git a/package-lock.json b/package-lock.json index 92bb2946c2..88362d5c60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,7 @@ "parse-server": "bin/parse-server" }, "devDependencies": { - "@actions/core": "1.11.1", + "@actions/core": "3.0.0", "@apollo/client": "3.13.8", "@babel/cli": "7.27.0", "@babel/core": "7.29.0", @@ -116,37 +116,38 @@ } }, "node_modules/@actions/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", - "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", + "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", "dev": true, "dependencies": { - "@actions/exec": "^1.1.1", - "@actions/http-client": "^2.0.1" + "@actions/exec": "^3.0.0", + "@actions/http-client": "^4.0.0" } }, "node_modules/@actions/exec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", - "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz", + "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==", "dev": true, "dependencies": { - "@actions/io": "^1.0.1" + "@actions/io": "^3.0.2" } }, "node_modules/@actions/http-client": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", - "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", "dev": true, "dependencies": { - "tunnel": "^0.0.6" + "tunnel": "^0.0.6", + "undici": "^6.23.0" } }, "node_modules/@actions/io": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", - "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz", + "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", "dev": true }, "node_modules/@apollo/cache-control-types": { @@ -21710,6 +21711,15 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, + "node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "dev": true, + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -22570,37 +22580,38 @@ }, "dependencies": { "@actions/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", - "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", + "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", "dev": true, "requires": { - "@actions/exec": "^1.1.1", - "@actions/http-client": "^2.0.1" + "@actions/exec": "^3.0.0", + "@actions/http-client": "^4.0.0" } }, "@actions/exec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", - "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz", + "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==", "dev": true, "requires": { - "@actions/io": "^1.0.1" + "@actions/io": "^3.0.2" } }, "@actions/http-client": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", - "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", "dev": true, "requires": { - "tunnel": "^0.0.6" + "tunnel": "^0.0.6", + "undici": "^6.23.0" } }, "@actions/io": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", - "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz", + "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", "dev": true }, "@apollo/cache-control-types": { @@ -37543,6 +37554,12 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, + "undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "dev": true + }, "undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/package.json b/package.json index dfa6097b34..f228b85b7b 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "ws": "8.18.2" }, "devDependencies": { - "@actions/core": "1.11.1", + "@actions/core": "3.0.0", "@apollo/client": "3.13.8", "@babel/cli": "7.27.0", "@babel/core": "7.29.0", From b6b632755263417c2a3c3a31381eedc516723740 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 12 Feb 2026 02:28:48 +0000 Subject: [PATCH 19/36] fix: Unlinking auth provider triggers auth data validation (#10045) --- spec/AuthenticationAdaptersV2.spec.js | 238 ++++++++++++++++++ src/Adapters/Auth/BaseCodeAuthAdapter.js | 23 +- .../Postgres/PostgresStorageAdapter.js | 20 +- src/Auth.js | 18 +- src/RestWrite.js | 10 +- 5 files changed, 296 insertions(+), 13 deletions(-) diff --git a/spec/AuthenticationAdaptersV2.spec.js b/spec/AuthenticationAdaptersV2.spec.js index e7bde12239..d8c646382c 100644 --- a/spec/AuthenticationAdaptersV2.spec.js +++ b/spec/AuthenticationAdaptersV2.spec.js @@ -1338,6 +1338,244 @@ describe('Auth Adapter features', () => { expect(user.get('authData')).toEqual({ adapterB: { id: 'test' } }); }); + it('should unlink a code-based auth provider without triggering adapter validation', async () => { + const mockUserId = 'gpgamesUser123'; + const mockAccessToken = 'mockAccessToken'; + + const otherAdapter = { + validateAppId: () => Promise.resolve(), + validateAuthData: () => Promise.resolve(), + }; + + mockFetch([ + { + url: 'https://oauth2.googleapis.com/token', + method: 'POST', + response: { + ok: true, + json: () => Promise.resolve({ access_token: mockAccessToken }), + }, + }, + { + url: `https://www.googleapis.com/games/v1/players/${mockUserId}`, + method: 'GET', + response: { + ok: true, + json: () => Promise.resolve({ playerId: mockUserId }), + }, + }, + ]); + + await reconfigureServer({ + auth: { + gpgames: { + clientId: 'testClientId', + clientSecret: 'testClientSecret', + }, + otherAdapter, + }, + }); + + // Sign up with username/password, then link providers + const user = new Parse.User(); + await user.signUp({ username: 'gpgamesTestUser', password: 'password123' }); + + // Link gpgames code-based provider + await user.save({ + authData: { + gpgames: { id: mockUserId, code: 'authCode123', redirect_uri: 'https://example.com/callback' }, + }, + }); + + // Link a second provider + await user.save({ authData: { otherAdapter: { id: 'other1' } } }); + + // Reset fetch spy to track calls during unlink + global.fetch.calls.reset(); + + // Unlink gpgames by setting authData to null; should not call beforeFind / external APIs + const sessionToken = user.getSessionToken(); + await user.save({ authData: { gpgames: null } }, { sessionToken }); + + // No external HTTP calls should have been made during unlink + expect(global.fetch.calls.count()).toBe(0); + + // Verify gpgames was removed while the other provider remains + await user.fetch({ useMasterKey: true }); + const authData = user.get('authData'); + expect(authData).toBeDefined(); + expect(authData.gpgames).toBeUndefined(); + expect(authData.otherAdapter).toEqual({ id: 'other1' }); + }); + + it('should unlink one code-based provider while echoing back another unchanged', async () => { + const gpgamesUserId = 'gpgamesUser1'; + const instagramUserId = 'igUser1'; + + // Mock gpgames API for initial login + mockFetch([ + { + url: 'https://oauth2.googleapis.com/token', + method: 'POST', + response: { + ok: true, + json: () => Promise.resolve({ access_token: 'gpgamesToken' }), + }, + }, + { + url: `https://www.googleapis.com/games/v1/players/${gpgamesUserId}`, + method: 'GET', + response: { + ok: true, + json: () => Promise.resolve({ playerId: gpgamesUserId }), + }, + }, + ]); + + await reconfigureServer({ + auth: { + gpgames: { + clientId: 'testClientId', + clientSecret: 'testClientSecret', + }, + instagram: { + clientId: 'testClientId', + clientSecret: 'testClientSecret', + redirectUri: 'https://example.com/callback', + }, + }, + }); + + // Login with gpgames + const user = await Parse.User.logInWith('gpgames', { + authData: { id: gpgamesUserId, code: 'gpCode1', redirect_uri: 'https://example.com/callback' }, + }); + const sessionToken = user.getSessionToken(); + + // Mock instagram API for linking + mockFetch([ + { + url: 'https://api.instagram.com/oauth/access_token', + method: 'POST', + response: { + ok: true, + json: () => Promise.resolve({ access_token: 'igToken' }), + }, + }, + { + url: `https://graph.instagram.com/me?fields=id&access_token=igToken`, + method: 'GET', + response: { + ok: true, + json: () => Promise.resolve({ id: instagramUserId }), + }, + }, + ]); + + // Link instagram as second provider + await user.save( + { authData: { instagram: { id: instagramUserId, code: 'igCode1' } } }, + { sessionToken } + ); + + // Fetch to get current authData (afterFind strips credentials, leaving only { id }) + await user.fetch({ sessionToken }); + const currentAuthData = user.get('authData'); + expect(currentAuthData.gpgames).toBeDefined(); + expect(currentAuthData.instagram).toBeDefined(); + + // Reset fetch spy + global.fetch.calls.reset(); + + // Unlink gpgames while echoing back instagram unchanged — the common client pattern: + // fetch current state, spread it, set the one to unlink to null + user.set('authData', { ...currentAuthData, gpgames: null }); + await user.save(null, { sessionToken }); + + // No external HTTP calls during unlink (no code exchange for unchanged instagram) + expect(global.fetch.calls.count()).toBe(0); + + // Verify gpgames removed, instagram preserved + await user.fetch({ useMasterKey: true }); + const finalAuthData = user.get('authData'); + expect(finalAuthData).toBeDefined(); + expect(finalAuthData.gpgames).toBeUndefined(); + expect(finalAuthData.instagram).toBeDefined(); + expect(finalAuthData.instagram.id).toBe(instagramUserId); + }); + + it('should reject changing an existing code-based provider id without credentials', async () => { + const mockUserId = 'gpgamesUser123'; + const mockAccessToken = 'mockAccessToken'; + + mockFetch([ + { + url: 'https://oauth2.googleapis.com/token', + method: 'POST', + response: { + ok: true, + json: () => Promise.resolve({ access_token: mockAccessToken }), + }, + }, + { + url: `https://www.googleapis.com/games/v1/players/${mockUserId}`, + method: 'GET', + response: { + ok: true, + json: () => Promise.resolve({ playerId: mockUserId }), + }, + }, + ]); + + await reconfigureServer({ + auth: { + gpgames: { + clientId: 'testClientId', + clientSecret: 'testClientSecret', + }, + }, + }); + + // Sign up and link gpgames with valid credentials + const user = new Parse.User(); + await user.save({ + authData: { + gpgames: { id: mockUserId, code: 'authCode123', redirect_uri: 'https://example.com/callback' }, + }, + }); + const sessionToken = user.getSessionToken(); + + // Attempt to change gpgames id without credentials (no code or access_token) + await expectAsync( + user.save({ authData: { gpgames: { id: 'differentUserId' } } }, { sessionToken }) + ).toBeRejectedWith( + jasmine.objectContaining({ message: jasmine.stringContaining('code is required') }) + ); + }); + + it('should reject linking a new code-based provider with only an id and no credentials', async () => { + await reconfigureServer({ + auth: { + gpgames: { + clientId: 'testClientId', + clientSecret: 'testClientSecret', + }, + }, + }); + + // Sign up with username/password (no gpgames linked) + const user = new Parse.User(); + await user.signUp({ username: 'linkTestUser', password: 'password123' }); + const sessionToken = user.getSessionToken(); + + // Attempt to link gpgames with only { id } — no code or access_token + await expectAsync( + user.save({ authData: { gpgames: { id: 'victimUserId' } } }, { sessionToken }) + ).toBeRejectedWith( + jasmine.objectContaining({ message: jasmine.stringContaining('code is required') }) + ); + }); + it('should handle multiple providers: add one while another remains unchanged (code-based)', async () => { await reconfigureServer({ auth: { diff --git a/src/Adapters/Auth/BaseCodeAuthAdapter.js b/src/Adapters/Auth/BaseCodeAuthAdapter.js index 696e4ee71b..52f12f8703 100644 --- a/src/Adapters/Auth/BaseCodeAuthAdapter.js +++ b/src/Adapters/Auth/BaseCodeAuthAdapter.js @@ -72,32 +72,47 @@ export default class BaseAuthCodeAdapter extends AuthAdapter { throw new Error('getAccessTokenFromCode is not implemented'); } + /** + * Validates auth data on login. In the standard auth flows (login, signup, + * update), `beforeFind` runs first and validates credentials, so no + * additional credential check is needed here. + */ validateLogin(authData) { - // User validation is already done in beforeFind return { id: authData.id, } } + /** + * Validates auth data on first setup or when linking a new provider. + * In the standard auth flows, `beforeFind` runs first and validates + * credentials, so no additional credential check is needed here. + */ validateSetUp(authData) { - // User validation is already done in beforeFind return { id: authData.id, } } + /** + * Returns the auth data to expose to the client after a query. + */ afterFind(authData) { return { id: authData.id, } } + /** + * Validates auth data on update. In the standard auth flows, `beforeFind` + * runs first for any changed auth data and validates credentials, so no + * additional credential check is needed here. Unchanged (echoed-back) data + * skips both `beforeFind` and validation entirely. + */ validateUpdate(authData) { - // User validation is already done in beforeFind return { id: authData.id, } - } parseResponseData(data) { diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 7eaafcbde2..e988c9cc19 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1607,20 +1607,28 @@ export class PostgresStorageAdapter implements StorageAdapter { const generate = (jsonb: string, key: string, value: any) => { return `json_object_set_key(COALESCE(${jsonb}, '{}'::jsonb), ${key}, ${value})::jsonb`; }; + const generateRemove = (jsonb: string, key: string) => { + return `(COALESCE(${jsonb}, '{}'::jsonb) - ${key})`; + }; const lastKey = `$${index}:name`; const fieldNameIndex = index; index += 1; values.push(fieldName); const update = Object.keys(fieldValue).reduce((lastKey: string, key: string) => { + let value = fieldValue[key]; + if (value && value.__op === 'Delete') { + value = null; + } + if (value === null) { + const str = generateRemove(lastKey, `$${index}::text`); + values.push(key); + index += 1; + return str; + } const str = generate(lastKey, `$${index}::text`, `$${index + 1}::jsonb`); index += 2; - let value = fieldValue[key]; if (value) { - if (value.__op === 'Delete') { - value = null; - } else { - value = JSON.stringify(value); - } + value = JSON.stringify(value); } values.push(key, value); return str; diff --git a/src/Auth.js b/src/Auth.js index 0601151ca4..331780961f 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -417,15 +417,29 @@ Auth.prototype._getAllRolesNamesForRoleIds = function (roleIDs, names = [], quer }); }; -const findUsersWithAuthData = async (config, authData, beforeFind) => { +const findUsersWithAuthData = async (config, authData, beforeFind, currentUserAuthData) => { const providers = Object.keys(authData); const queries = await Promise.all( providers.map(async provider => { const providerAuthData = authData[provider]; + // Skip providers being unlinked (null value) + if (providerAuthData === null) { + return null; + } + + // Skip beforeFind only when incoming data is confirmed unchanged from stored data. + // This handles echoed-back authData from afterFind (e.g. client sends back { id: 'x' } + // alongside a provider unlink). On login/signup, currentUserAuthData is undefined so + // beforeFind always runs, preserving it as the security gate for missing credentials. + const storedProviderData = currentUserAuthData?.[provider]; + const incomingKeys = Object.keys(providerAuthData || {}); + const isUnchanged = storedProviderData && incomingKeys.length > 0 && + !incomingKeys.some(key => !isDeepStrictEqual(providerAuthData[key], storedProviderData[key])); + const adapter = config.authDataManager.getValidatorForProvider(provider)?.adapter; - if (beforeFind && typeof adapter?.beforeFind === 'function') { + if (beforeFind && typeof adapter?.beforeFind === 'function' && !isUnchanged) { await adapter.beforeFind(providerAuthData); } diff --git a/src/RestWrite.js b/src/RestWrite.js index 6630a81e85..5eee53a937 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -541,7 +541,15 @@ RestWrite.prototype.ensureUniqueAuthDataId = async function () { }; RestWrite.prototype.handleAuthData = async function (authData) { - const r = await Auth.findUsersWithAuthData(this.config, authData, true); + let currentUserAuthData; + if (this.query?.objectId) { + const [currentUser] = await this.config.database.find( + '_User', + { objectId: this.query.objectId } + ); + currentUserAuthData = currentUser?.authData; + } + const r = await Auth.findUsersWithAuthData(this.config, authData, true, currentUserAuthData); const results = this.filteredObjectsByACL(r); const userId = this.getUserId(); From 506449412b289acb10f7d1588332f8a8702b8ef9 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 12 Feb 2026 02:29:34 +0000 Subject: [PATCH 20/36] chore(release): 9.3.0-alpha.4 [skip ci] # [9.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.3...9.3.0-alpha.4) (2026-02-12) ### Bug Fixes * Unlinking auth provider triggers auth data validation ([#10045](https://github.com/parse-community/parse-server/issues/10045)) ([b6b6327](https://github.com/parse-community/parse-server/commit/b6b632755263417c2a3c3a31381eedc516723740)) --- 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 4ce144e163..f2df26170f 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [9.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.3...9.3.0-alpha.4) (2026-02-12) + + +### Bug Fixes + +* Unlinking auth provider triggers auth data validation ([#10045](https://github.com/parse-community/parse-server/issues/10045)) ([b6b6327](https://github.com/parse-community/parse-server/commit/b6b632755263417c2a3c3a31381eedc516723740)) + # [9.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.2...9.3.0-alpha.3) (2026-02-07) diff --git a/package-lock.json b/package-lock.json index 88362d5c60..d2e6b3de02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.3.0-alpha.3", + "version": "9.3.0-alpha.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.3.0-alpha.3", + "version": "9.3.0-alpha.4", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index f228b85b7b..2037253cf5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.3.0-alpha.3", + "version": "9.3.0-alpha.4", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 1af6c0dc1fc29ff738e31c10b4ec99ed410500c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:56:06 +0000 Subject: [PATCH 21/36] refactor: Bump yaml from 2.8.0 to 2.8.2 (#10051) --- package-lock.json | 18 ++++++++++-------- package.json | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index d2e6b3de02..3e285b5e52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,7 +102,7 @@ "semantic-release": "24.2.5", "typescript": "5.8.3", "typescript-eslint": "8.53.1", - "yaml": "2.8.0" + "yaml": "2.8.2" }, "engines": { "node": ">=20.19.0 <21.0.0 || >=22.12.0 <23.0.0 || >=24.11.0 <25.0.0" @@ -22411,16 +22411,18 @@ "dev": true }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, - "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { @@ -38066,9 +38068,9 @@ "dev": true }, "yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true }, "yargs": { diff --git a/package.json b/package.json index 2037253cf5..ef2d0c82d6 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "semantic-release": "24.2.5", "typescript": "5.8.3", "typescript-eslint": "8.53.1", - "yaml": "2.8.0" + "yaml": "2.8.2" }, "scripts": { "ci:check": "node ./ci/ciCheck.js", From 44a5bb105e11e6918e899e0f1427b0adb38d6d67 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:45:41 +0000 Subject: [PATCH 22/36] fix: `Parse.Query.select('authData')` for `_User` class doesn't return auth data (#10055) --- spec/ParseUser.spec.js | 13 +++++++++++++ src/Adapters/Auth/index.js | 17 +++++++++++++++++ src/RestQuery.js | 20 ++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index aaae271332..48ae1d2a9c 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -1395,6 +1395,19 @@ describe('Parse.User testing', () => { }); }); + it('should return authData when select authData with masterKey', async () => { + const provider = getMockFacebookProvider(); + Parse.User._registerAuthenticationProvider(provider); + const user = await Parse.User._logInWith('facebook'); + const query = new Parse.Query(Parse.User); + query.select('authData'); + const result = await query.get(user.id, { useMasterKey: true }); + expect(result.get('authData')).toBeDefined(); + expect(result.get('authData').facebook).toBeDefined(); + expect(result.get('authData').facebook.id).toBe('8675309'); + expect(result.get('authData').facebook.access_token).toBe('jenny'); + }); + it('only creates a single session for an installation / user pair (#2885)', async done => { Parse.Object.disableSingleInstance(); const provider = getMockFacebookProvider(); diff --git a/src/Adapters/Auth/index.js b/src/Adapters/Auth/index.js index 7f5581da49..7c4f0e7011 100755 --- a/src/Adapters/Auth/index.js +++ b/src/Adapters/Auth/index.js @@ -254,8 +254,25 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) { ); }; + // Returns the list of auth provider names that have a valid adapter configured. + // This includes both built-in providers and custom providers from authOptions. + const getProviders = function () { + const allProviders = new Set([...Object.keys(providers), ...Object.keys(authOptions)]); + if (!_enableAnonymousUsers) { + allProviders.delete('anonymous'); + } + return [...allProviders].filter(provider => { + try { + return !!loadAuthAdapter(provider, authOptions); + } catch { + return false; + } + }); + }; + return Object.freeze({ getValidatorForProvider, + getProviders, setEnableAnonymousUsers, runAfterFind, }); diff --git a/src/RestQuery.js b/src/RestQuery.js index 2064ffd0df..76535d5edc 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -751,6 +751,26 @@ _UnsafeRestQuery.prototype.runFind = async function (options = {}) { findOptions.keys = this.keys.map(key => { return key.split('.')[0]; }); + // When selecting `authData` on `_User`, also add the internal auth data fields + // (e.g. `_auth_data_facebook`) for each configured auth provider. In MongoDB, + // `authData` is stored as individual `_auth_data_` fields, so the + // projection for `authData` alone won't match them. Adding both ensures it + // works across all database adapters: Mongo uses `_auth_data_*` fields, + // Postgres uses the `authData` column directly. + // + // Note: When selecting `authData`, only auth data of currently configured + // providers is returned. Auth data entries of providers that are no longer + // configured won't be included. To return all auth data regardless of the + // provider configuration, do not use `authData` as a selected key. + if (this.className === '_User' && findOptions.keys.includes('authData')) { + const providers = this.config.authDataManager.getProviders(); + for (const provider of providers) { + const key = `_auth_data_${provider}`; + if (!findOptions.keys.includes(key)) { + findOptions.keys.push(key); + } + } + } } if (options.op) { findOptions.op = options.op; From c7f57f7032606202be4782d1ddeb9c0f76c44b1b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 12 Feb 2026 20:46:35 +0000 Subject: [PATCH 23/36] chore(release): 9.3.0-alpha.5 [skip ci] # [9.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.4...9.3.0-alpha.5) (2026-02-12) ### Bug Fixes * `Parse.Query.select('authData')` for `_User` class doesn't return auth data ([#10055](https://github.com/parse-community/parse-server/issues/10055)) ([44a5bb1](https://github.com/parse-community/parse-server/commit/44a5bb105e11e6918e899e0f1427b0adb38d6d67)) --- 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 f2df26170f..8e3102e3af 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [9.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.4...9.3.0-alpha.5) (2026-02-12) + + +### Bug Fixes + +* `Parse.Query.select('authData')` for `_User` class doesn't return auth data ([#10055](https://github.com/parse-community/parse-server/issues/10055)) ([44a5bb1](https://github.com/parse-community/parse-server/commit/44a5bb105e11e6918e899e0f1427b0adb38d6d67)) + # [9.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.3...9.3.0-alpha.4) (2026-02-12) diff --git a/package-lock.json b/package-lock.json index 3e285b5e52..0a2c6c4521 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.3.0-alpha.4", + "version": "9.3.0-alpha.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.3.0-alpha.4", + "version": "9.3.0-alpha.5", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index ef2d0c82d6..236c9a3135 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.3.0-alpha.4", + "version": "9.3.0-alpha.5", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 403d9a665a9c60dff728ccb15ae50ec67f98b741 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:12:04 +0000 Subject: [PATCH 24/36] refactor: Bump qs from 6.14.1 to 6.14.2 (#10056) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a2c6c4521..bd6ead29d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18960,9 +18960,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dependencies": { "side-channel": "^1.1.0" }, @@ -35646,9 +35646,9 @@ "dev": true }, "qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "requires": { "side-channel": "^1.1.0" } From 58fac7813dc20bac1b3b978e8f70c01506658f02 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:35:10 +0000 Subject: [PATCH 25/36] refactor: Bump @babel/plugin-transform-flow-strip-types from 7.26.5 to 7.27.1 (#10054) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 53 ++++++++++++++++++++++------------------------- package.json | 2 +- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd6ead29d3..a3a1099738 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,7 @@ "@babel/core": "7.29.0", "@babel/eslint-parser": "7.28.6", "@babel/plugin-proposal-object-rest-spread": "7.20.7", - "@babel/plugin-transform-flow-strip-types": "7.26.5", + "@babel/plugin-transform-flow-strip-types": "7.27.1", "@babel/preset-env": "7.27.2", "@babel/preset-typescript": "7.27.1", "@saithodev/semantic-release-backmerge": "4.0.1", @@ -803,11 +803,10 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -1051,13 +1050,12 @@ } }, "node_modules/@babel/plugin-syntax-flow": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz", - "integrity": "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", + "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1439,14 +1437,13 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.26.5.tgz", - "integrity": "sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", + "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/plugin-syntax-flow": "^7.26.0" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-flow": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -23076,9 +23073,9 @@ } }, "@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true }, "@babel/helper-remap-async-to-generator": { @@ -23231,12 +23228,12 @@ "requires": {} }, "@babel/plugin-syntax-flow": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz", - "integrity": "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", + "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-syntax-import-assertions": { @@ -23461,13 +23458,13 @@ } }, "@babel/plugin-transform-flow-strip-types": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.26.5.tgz", - "integrity": "sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", + "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/plugin-syntax-flow": "^7.26.0" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-flow": "^7.27.1" } }, "@babel/plugin-transform-for-of": { diff --git a/package.json b/package.json index 236c9a3135..59ad20acbb 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@babel/core": "7.29.0", "@babel/eslint-parser": "7.28.6", "@babel/plugin-proposal-object-rest-spread": "7.20.7", - "@babel/plugin-transform-flow-strip-types": "7.26.5", + "@babel/plugin-transform-flow-strip-types": "7.27.1", "@babel/preset-env": "7.27.2", "@babel/preset-typescript": "7.27.1", "@saithodev/semantic-release-backmerge": "4.0.1", From 12e10e265764a77cb637b17eff8ec141f9494284 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:14:08 +0000 Subject: [PATCH 26/36] refactor: Bump typescript from 5.8.3 to 5.9.3 (#10053) --- package-lock.json | 15 +++++++-------- package.json | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index a3a1099738..e0bffdd419 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,7 +100,7 @@ "nyc": "17.1.0", "prettier": "3.8.1", "semantic-release": "24.2.5", - "typescript": "5.8.3", + "typescript": "5.9.3", "typescript-eslint": "8.53.1", "yaml": "2.8.2" }, @@ -21519,11 +21519,10 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -37435,9 +37434,9 @@ } }, "typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true }, "typescript-eslint": { diff --git a/package.json b/package.json index 59ad20acbb..629cadc163 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "nyc": "17.1.0", "prettier": "3.8.1", "semantic-release": "24.2.5", - "typescript": "5.8.3", + "typescript": "5.9.3", "typescript-eslint": "8.53.1", "yaml": "2.8.2" }, From 8cfc85642486cdd8a14a7ccd953422c4976b8cfa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:20:12 +0000 Subject: [PATCH 27/36] refactor: Bump pg-monitor from 3.0.0 to 3.1.0 (#10052) --- package-lock.json | 21 ++++++++++----------- package.json | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index e0bffdd419..ca5a30cab7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "otpauth": "9.4.0", "parse": "8.0.3", "path-to-regexp": "8.3.0", - "pg-monitor": "3.0.0", + "pg-monitor": "3.1.0", "pg-promise": "12.6.0", "pluralize": "8.0.0", "punycode": "2.3.1", @@ -18391,15 +18391,14 @@ } }, "node_modules/pg-monitor": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pg-monitor/-/pg-monitor-3.0.0.tgz", - "integrity": "sha512-62jezmq3lR+lKCIsi9BXVg8Fxv+JG5LtaAuUmex5EVnBPlvAU7Ad6dOiQXHtH1xNh/Oy6Hux36k8uIjZWNeWtQ==", - "license": "MIT", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pg-monitor/-/pg-monitor-3.1.0.tgz", + "integrity": "sha512-giK0h52AOO/v8iu6hZCdZ/X9W8oAM9Dm1VReQQtki532X8g4z1LVIm4Z/3cGvDcETWW+Ty0FrtU8iTrGFYIZfA==", "dependencies": { - "picocolors": "^1.1.1" + "picocolors": "1.1.1" }, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/pg-pool": { @@ -35244,11 +35243,11 @@ "integrity": "sha512-jO/oJOununpx8DzKgvSsWm61P8JjwXlaxSlbbfTBo1nvSWoo/+I6qZYaSN96jm/KDwa5d+JMQwPGgcP6HXDRow==" }, "pg-monitor": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pg-monitor/-/pg-monitor-3.0.0.tgz", - "integrity": "sha512-62jezmq3lR+lKCIsi9BXVg8Fxv+JG5LtaAuUmex5EVnBPlvAU7Ad6dOiQXHtH1xNh/Oy6Hux36k8uIjZWNeWtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pg-monitor/-/pg-monitor-3.1.0.tgz", + "integrity": "sha512-giK0h52AOO/v8iu6hZCdZ/X9W8oAM9Dm1VReQQtki532X8g4z1LVIm4Z/3cGvDcETWW+Ty0FrtU8iTrGFYIZfA==", "requires": { - "picocolors": "^1.1.1" + "picocolors": "1.1.1" } }, "pg-pool": { diff --git a/package.json b/package.json index 629cadc163..fa503aa2a7 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "otpauth": "9.4.0", "parse": "8.0.3", "path-to-regexp": "8.3.0", - "pg-monitor": "3.0.0", + "pg-monitor": "3.1.0", "pg-promise": "12.6.0", "pluralize": "8.0.0", "punycode": "2.3.1", From 4ef89d912c08bb24500a4d4142a3220f024a2d34 Mon Sep 17 00:00:00 2001 From: Yazan Date: Sat, 14 Feb 2026 04:56:42 +0300 Subject: [PATCH 28/36] fix: Default ACL overwrites custom ACL on `Parse.Object` update (#10061) --- spec/ParseACL.spec.js | 62 +++++++++++++++++++++++++++++++++++++++++++ src/RestWrite.js | 4 +-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index d8abc65c06..f2d2cbd8b3 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -951,4 +951,66 @@ describe('Parse.ACL', () => { expect(acl[user.id].write).toBeTrue(); expect(acl[user.id].read).toBeTrue(); }); + + it('should not overwrite ACL with defaultACL on update', async () => { + await new Parse.Object('TestObject').save(); + const schema = await Parse.Server.database.loadSchema(); + await schema.updateClass( + 'TestObject', + {}, + { + create: { '*': true }, + update: { '*': true }, + addField: { '*': true }, + ACL: { + '*': { read: true }, + currentUser: { read: true, write: true }, + }, + } + ); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + const obj = new Parse.Object('TestObject'); + await obj.save(null, { sessionToken: user.getSessionToken() }); + + const originalAcl = obj.getACL().toJSON(); + expect(originalAcl['*']).toEqual({ read: true }); + expect(originalAcl[user.id]).toEqual({ read: true, write: true }); + + obj.set('field', 'value'); + await obj.save(null, { sessionToken: user.getSessionToken() }); + + const updatedAcl = obj.getACL().toJSON(); + expect(updatedAcl).toEqual(originalAcl); + }); + + it('should allow explicit ACL modification on update', async () => { + await new Parse.Object('TestObject').save(); + const schema = await Parse.Server.database.loadSchema(); + await schema.updateClass( + 'TestObject', + {}, + { + create: { '*': true }, + update: { '*': true }, + ACL: { + '*': { read: true }, + currentUser: { read: true, write: true }, + }, + } + ); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + const obj = new Parse.Object('TestObject'); + await obj.save(null, { sessionToken: user.getSessionToken() }); + + const customAcl = new Parse.ACL(); + customAcl.setPublicReadAccess(false); + customAcl.setReadAccess(user.id, true); + customAcl.setWriteAccess(user.id, true); + obj.setACL(customAcl); + await obj.save(null, { sessionToken: user.getSessionToken() }); + + const updatedAcl = obj.getACL().toJSON(); + expect(updatedAcl['*']).toBeUndefined(); + expect(updatedAcl[user.id]).toEqual({ read: true, write: true }); + }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index 5eee53a937..b8d0e670eb 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -370,8 +370,8 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function () { } }; - // add default ACL - if ( + // add default ACL (only on CREATE, not UPDATE) + if (!this.query && schema?.classLevelPermissions?.ACL && !this.data.ACL && JSON.stringify(schema.classLevelPermissions.ACL) !== From 1ac6011aa873e2bee2c8e87d7bb5faf0559ffa2e Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 14 Feb 2026 01:57:34 +0000 Subject: [PATCH 29/36] chore(release): 9.3.0-alpha.6 [skip ci] # [9.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.5...9.3.0-alpha.6) (2026-02-14) ### Bug Fixes * Default ACL overwrites custom ACL on `Parse.Object` update ([#10061](https://github.com/parse-community/parse-server/issues/10061)) ([4ef89d9](https://github.com/parse-community/parse-server/commit/4ef89d912c08bb24500a4d4142a3220f024a2d34)) --- 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 8e3102e3af..57853d25ba 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [9.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.5...9.3.0-alpha.6) (2026-02-14) + + +### Bug Fixes + +* Default ACL overwrites custom ACL on `Parse.Object` update ([#10061](https://github.com/parse-community/parse-server/issues/10061)) ([4ef89d9](https://github.com/parse-community/parse-server/commit/4ef89d912c08bb24500a4d4142a3220f024a2d34)) + # [9.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.4...9.3.0-alpha.5) (2026-02-12) diff --git a/package-lock.json b/package-lock.json index ca5a30cab7..3c650d34c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.3.0-alpha.5", + "version": "9.3.0-alpha.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.3.0-alpha.5", + "version": "9.3.0-alpha.6", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index fa503aa2a7..3861630153 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.3.0-alpha.5", + "version": "9.3.0-alpha.6", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 8b5a14ecaf0b58b899651fb97d43e0e5d9be506d Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:00:21 +0000 Subject: [PATCH 30/36] feat: Upgrade to parse 8.2.0, @parse/push-adapter 8.3.0 (#10066) --- package-lock.json | 1284 +++++++++++++++++++++++++++++---------------- package.json | 4 +- 2 files changed, 834 insertions(+), 454 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c650d34c4..a1a1ec7d55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@graphql-tools/schema": "10.0.23", "@graphql-tools/utils": "10.8.6", "@parse/fs-files-adapter": "3.0.0", - "@parse/push-adapter": "8.2.0", + "@parse/push-adapter": "8.3.0", "bcryptjs": "3.0.3", "commander": "14.0.3", "cors": "2.8.6", @@ -38,7 +38,7 @@ "mongodb": "7.0.0", "mustache": "4.2.0", "otpauth": "9.4.0", - "parse": "8.0.3", + "parse": "8.2.0", "path-to-regexp": "8.3.0", "pg-monitor": "3.1.0", "pg-promise": "12.6.0", @@ -2175,6 +2175,18 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.0.tgz", + "integrity": "sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.48.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -2602,9 +2614,9 @@ } }, "node_modules/@google-cloud/storage": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.18.0.tgz", - "integrity": "sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", + "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -2614,7 +2626,7 @@ "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", - "fast-xml-parser": "^4.4.1", + "fast-xml-parser": "^5.3.4", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", @@ -2712,9 +2724,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.2.tgz", - "integrity": "sha512-QzVUtEFyu05UNx2xr0fCQmStUO17uVQhGNowtxs00IgTZT6/W2PBLfUkj30s0FKJ29VtTa3ArVNIhNP6akQhqA==", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -3042,7 +3054,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, + "devOptional": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -3059,7 +3071,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=12" }, @@ -3071,7 +3083,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, + "devOptional": true, "engines": { "node": ">=12" }, @@ -3083,13 +3095,13 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "devOptional": true }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, + "devOptional": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -3106,7 +3118,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -3121,7 +3133,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -4085,13 +4097,13 @@ "integrity": "sha512-Bb+qLtXQ/1SA2Ck6JLVhfD9JQf6cCwgeDZZJjcIdHzUtdPTFu1hj51xdD7tUCL47Ed2i3aAx6K/M6AjLWYVs3A==" }, "node_modules/@parse/node-apn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-7.0.1.tgz", - "integrity": "sha512-2xBiaznvupLOoXFaxWxcWcqCGlRn9rvqeAQnv8ogL8hZPe1Rd0es+F8ppE7g4QIy5DPJv0R4fruB8amGM6K/qA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-7.1.0.tgz", + "integrity": "sha512-a40P5nScLDi9Pf7koKKkbwI73px0q+iLaKYNrr7kyKJebq/4duGOy3mMevZS0zltn171k3jB5BWCC27dPGsMmw==", "license": "MIT", "dependencies": { "debug": "4.4.3", - "jsonwebtoken": "9.0.2", + "jsonwebtoken": "9.0.3", "node-forge": "1.3.2", "verror": "1.10.1" }, @@ -4099,17 +4111,60 @@ "node": ">=18" } }, + "node_modules/@parse/node-apn/node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/@parse/node-apn/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@parse/node-apn/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/@parse/push-adapter": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-8.2.0.tgz", - "integrity": "sha512-z5RB1TwNELNSvummTVP1fgncOT424j13HeKxsGHpAftUmjE+hUtSsIeG49chD0gac22Zmrk+7flYjRwQpUs6+w==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-8.3.0.tgz", + "integrity": "sha512-snHVH0j5pqleU3uBUjHCzTR1ex8FFnAaDbKX3Bz+L6w2mLT2Zio2e7fxxrDCorluVBVJvorSsSjgZQsWCXL+Cg==", "license": "MIT", "dependencies": { - "@parse/node-apn": "7.0.1", - "expo-server-sdk": "4.0.0", - "firebase-admin": "13.6.0", + "@parse/node-apn": "7.1.0", + "expo-server-sdk": "5.0.0", + "firebase-admin": "13.6.1", "npmlog": "7.0.1", - "parse": "8.0.3", + "parse": "8.2.0", "web-push": "3.6.7" }, "engines": { @@ -4647,18 +4702,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@saithodev/semantic-release-backmerge/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@saithodev/semantic-release-backmerge/node_modules/ansi-escapes": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", @@ -4943,19 +4986,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@saithodev/semantic-release-backmerge/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@saithodev/semantic-release-backmerge/node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -5443,18 +5473,6 @@ "node": ">=18" } }, - "node_modules/@semantic-release/github/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@semantic-release/github/node_modules/aggregate-error": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", @@ -5518,19 +5536,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/github/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@semantic-release/github/node_modules/indent-string": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", @@ -7149,15 +7154,12 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "optional": true, - "dependencies": { - "debug": "4" - }, + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/aggregate-error": { @@ -7529,7 +7531,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "devOptional": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -7566,9 +7568,10 @@ "license": "Apache-2.0" }, "node_modules/bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", "engines": { "node": "*" } @@ -8720,9 +8723,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.46.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.46.0.tgz", - "integrity": "sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==", + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.48.0.tgz", + "integrity": "sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -8812,7 +8815,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "devOptional": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -8858,7 +8861,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", - "dev": true, + "devOptional": true, "engines": { "node": ">= 12" } @@ -9468,7 +9471,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "devOptional": true }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", @@ -10241,59 +10244,26 @@ } }, "node_modules/expo-server-sdk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/expo-server-sdk/-/expo-server-sdk-4.0.0.tgz", - "integrity": "sha512-zi83XtG2pqyP3gyn1JIRYkydo2i6HU3CYaWo/VvhZG/F29U+QIDv6LBEUsWf4ddZlVE7c9WN1N8Be49rHgO8OQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/expo-server-sdk/-/expo-server-sdk-5.0.0.tgz", + "integrity": "sha512-GEp1XYLU80iS/hdRo3c2n092E8TgTXcHSuw6Lw68dSoWaAgiLPI2R+e5hp5+hGF1TtJZOi2nxtJX63+XA3iz9g==", "license": "MIT", "dependencies": { - "node-fetch": "^2.6.0", "promise-limit": "^2.7.0", - "promise-retry": "^2.0.1" + "promise-retry": "^2.0.1", + "undici": "^7.2.0" }, "engines": { "node": ">=20" } }, - "node_modules/expo-server-sdk/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/expo-server-sdk/node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/expo-server-sdk/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/expo-server-sdk/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/expo-server-sdk/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "node": ">=20.18.1" } }, "node_modules/express": { @@ -10377,7 +10347,8 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" }, "node_modules/extract-files": { "version": "13.0.0", @@ -10462,9 +10433,9 @@ "dev": true }, "node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.7.tgz", + "integrity": "sha512-JzVLro9NQv92pOM/jTCR6mHlJh2FGwtomH8ZQjhFj/R29P2Fnj38OgPJVtcvYw6SuKClhgYuwUZf5b3rd8u2mA==", "funding": [ { "type": "github", @@ -10474,7 +10445,7 @@ "license": "MIT", "optional": true, "dependencies": { - "strnum": "^1.1.1" + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -10520,7 +10491,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -10843,9 +10814,9 @@ } }, "node_modules/firebase-admin": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.6.0.tgz", - "integrity": "sha512-GdPA/t0+Cq8p1JnjFRBmxRxAGvF/kl2yfdhALl38PrRp325YxyQ5aNaHui0XmaKcKiGRFIJ/EgBNWFoDP0onjw==", + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.6.1.tgz", + "integrity": "sha512-Zgc6yPtmPxAZo+FoK6LMG6zpSEsoSK8ifIR+IqF4oWuC3uWZU40OjxgfLTSFcsRlj/k/wD66zNv2UiTRreCNSw==", "license": "Apache-2.0", "dependencies": { "@fastify/busboy": "^3.0.0", @@ -10970,7 +10941,7 @@ "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dev": true, + "devOptional": true, "dependencies": { "fetch-blob": "^3.1.2" }, @@ -11177,28 +11148,6 @@ "node": ">=14" } }, - "node_modules/gaxios/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/gaxios/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/gaxios/node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -11254,6 +11203,170 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/gcp-metadata": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/gcp-metadata/node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gcp-metadata/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gcp-metadata/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/gcp-metadata/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/gcp-metadata/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gcp-metadata/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -11548,6 +11661,15 @@ "node": ">=14" } }, + "node_modules/google-auth-library/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/google-auth-library/node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -11563,6 +11685,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" @@ -11653,10 +11776,12 @@ } }, "node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=14" } @@ -11805,6 +11930,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" @@ -12063,18 +12189,6 @@ "node": ">= 14" } }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/http2-wrapper": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", @@ -12090,16 +12204,16 @@ } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "optional": true, + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/human-signals": { @@ -12509,7 +12623,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "devOptional": true }, "node_modules/issue-parser": { "version": "7.0.1", @@ -12697,7 +12811,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, + "devOptional": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -12939,6 +13053,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", "dependencies": { "bignumber.js": "^9.0.0" } @@ -14310,7 +14425,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -14549,6 +14664,21 @@ "mongodb-runner": "bin/runner.js" } }, + "node_modules/mongodb-runner/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/mongodb-runner/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -14605,6 +14735,56 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/mongodb-runner/node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-runner/node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-runner/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/mongodb-runner/node_modules/mongodb": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", @@ -14651,6 +14831,60 @@ } } }, + "node_modules/mongodb-runner/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/mongodb-runner/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/mongodb-runner/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true + }, + "node_modules/mongodb-runner/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/mongodb-runner/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -14819,7 +15053,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -18104,7 +18338,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true + "devOptional": true }, "node_modules/param-case": { "version": "3.0.4", @@ -18129,13 +18363,13 @@ } }, "node_modules/parse": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/parse/-/parse-8.0.3.tgz", - "integrity": "sha512-WQPrnfnXy6/p25OFD6qOAVK9hIhhU882Nw1AW5RjAJbO2G7YqChJxBgL94aexsaTnP9ajVzjGISSQ+mESrkMIA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/parse/-/parse-8.2.0.tgz", + "integrity": "sha512-jSx4zIqCja6O2HKhkzZ6JTm4fBUQS6sQpvFCAsqzaU4XlEhoRLm9mM1tZeYhvxTVA6zymvj/EJZ4YDOXCTRbmA==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "7.28.6", - "@babel/runtime-corejs3": "7.28.6", + "@babel/runtime-corejs3": "7.29.0", "crypto-js": "4.2.0", "idb-keyval": "6.2.2", "react-native-crypto-js": "1.0.0", @@ -18172,18 +18406,6 @@ "node": ">=6" } }, - "node_modules/parse/node_modules/@babel/runtime-corejs3": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.6.tgz", - "integrity": "sha512-kz2fAQ5UzjV7X7D3ySxmj3vRq89dTpqOZWv76Z6pNPztkwb/0Yj1Mtx1xFrYj6mbIHysxtBot8J4o0JLCblcFw==", - "license": "MIT", - "dependencies": { - "core-js-pure": "^3.43.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/parse/node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -18269,7 +18491,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, + "devOptional": true, "engines": { "node": ">=8" } @@ -18284,7 +18506,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, + "devOptional": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -20206,7 +20428,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, + "devOptional": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -20218,7 +20440,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, + "devOptional": true, "engines": { "node": ">=8" } @@ -20650,7 +20872,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -20699,7 +20921,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -20748,9 +20970,9 @@ } }, "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", "funding": [ { "type": "github", @@ -20997,6 +21219,19 @@ "node": ">=14" } }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/teeny-request/node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -21012,6 +21247,20 @@ "node": ">= 6" } }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/teeny-request/node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -21991,29 +22240,6 @@ "node": ">= 16" } }, - "node_modules/web-push/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/web-push/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/web-push/node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -22037,7 +22263,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">= 8" } @@ -22098,7 +22324,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "devOptional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -22262,7 +22488,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -22279,7 +22505,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -22294,7 +22520,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, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -22306,7 +22532,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", @@ -23921,6 +24147,14 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==" }, + "@babel/runtime-corejs3": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.0.tgz", + "integrity": "sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==", + "requires": { + "core-js-pure": "^3.48.0" + } + }, "@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -24236,9 +24470,9 @@ "optional": true }, "@google-cloud/storage": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.18.0.tgz", - "integrity": "sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", + "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", "optional": true, "requires": { "@google-cloud/paginator": "^5.0.0", @@ -24247,7 +24481,7 @@ "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", - "fast-xml-parser": "^4.4.1", + "fast-xml-parser": "^5.3.4", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", @@ -24310,9 +24544,9 @@ "requires": {} }, "@grpc/grpc-js": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.2.tgz", - "integrity": "sha512-QzVUtEFyu05UNx2xr0fCQmStUO17uVQhGNowtxs00IgTZT6/W2PBLfUkj30s0FKJ29VtTa3ArVNIhNP6akQhqA==", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "optional": true, "requires": { "@grpc/proto-loader": "^0.8.0", @@ -24533,7 +24767,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, + "devOptional": true, "requires": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -24547,25 +24781,25 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true + "devOptional": true }, "ansi-styles": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true + "devOptional": true }, "emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "devOptional": true }, "string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, + "devOptional": true, "requires": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -24576,7 +24810,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, + "devOptional": true, "requires": { "ansi-regex": "^6.0.1" } @@ -24585,7 +24819,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, + "devOptional": true, "requires": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -25301,26 +25535,64 @@ "integrity": "sha512-Bb+qLtXQ/1SA2Ck6JLVhfD9JQf6cCwgeDZZJjcIdHzUtdPTFu1hj51xdD7tUCL47Ed2i3aAx6K/M6AjLWYVs3A==" }, "@parse/node-apn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-7.0.1.tgz", - "integrity": "sha512-2xBiaznvupLOoXFaxWxcWcqCGlRn9rvqeAQnv8ogL8hZPe1Rd0es+F8ppE7g4QIy5DPJv0R4fruB8amGM6K/qA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-7.1.0.tgz", + "integrity": "sha512-a40P5nScLDi9Pf7koKKkbwI73px0q+iLaKYNrr7kyKJebq/4duGOy3mMevZS0zltn171k3jB5BWCC27dPGsMmw==", "requires": { "debug": "4.4.3", - "jsonwebtoken": "9.0.2", + "jsonwebtoken": "9.0.3", "node-forge": "1.3.2", "verror": "1.10.1" + }, + "dependencies": { + "jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "requires": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + } + }, + "jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "requires": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + } } }, "@parse/push-adapter": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-8.2.0.tgz", - "integrity": "sha512-z5RB1TwNELNSvummTVP1fgncOT424j13HeKxsGHpAftUmjE+hUtSsIeG49chD0gac22Zmrk+7flYjRwQpUs6+w==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-8.3.0.tgz", + "integrity": "sha512-snHVH0j5pqleU3uBUjHCzTR1ex8FFnAaDbKX3Bz+L6w2mLT2Zio2e7fxxrDCorluVBVJvorSsSjgZQsWCXL+Cg==", "requires": { - "@parse/node-apn": "7.0.1", - "expo-server-sdk": "4.0.0", - "firebase-admin": "13.6.0", + "@parse/node-apn": "7.1.0", + "expo-server-sdk": "5.0.0", + "firebase-admin": "13.6.1", "npmlog": "7.0.1", - "parse": "8.0.3", + "parse": "8.2.0", "web-push": "3.6.7" } }, @@ -25736,15 +26008,6 @@ } } }, - "agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "requires": { - "debug": "^4.3.4" - } - }, "ansi-escapes": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", @@ -25932,16 +26195,6 @@ } } }, - "https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dev": true, - "requires": { - "agent-base": "^7.0.2", - "debug": "4" - } - }, "human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -26277,15 +26530,6 @@ "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", "dev": true }, - "agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "requires": { - "debug": "^4.3.4" - } - }, "aggregate-error": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", @@ -26325,16 +26569,6 @@ "unicorn-magic": "^0.1.0" } }, - "https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dev": true, - "requires": { - "agent-base": "^7.0.2", - "debug": "4" - } - }, "indent-string": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", @@ -27422,13 +27656,9 @@ "requires": {} }, "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "optional": true, - "requires": { - "debug": "4" - } + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==" }, "aggregate-error": { "version": "3.1.0", @@ -27711,7 +27941,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "devOptional": true }, "base64-js": { "version": "1.5.1", @@ -27730,9 +27960,9 @@ "dev": true }, "bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==" }, "binary-extensions": { "version": "2.2.0", @@ -28546,9 +28776,9 @@ } }, "core-js-pure": { - "version": "3.46.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.46.0.tgz", - "integrity": "sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==" + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.48.0.tgz", + "integrity": "sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==" }, "core-util-is": { "version": "1.0.3", @@ -28598,7 +28828,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "devOptional": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -28631,7 +28861,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", - "dev": true + "devOptional": true }, "debug": { "version": "4.4.3", @@ -29076,7 +29306,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "devOptional": true }, "ecdsa-sig-formatter": { "version": "1.0.11", @@ -29607,41 +29837,19 @@ } }, "expo-server-sdk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/expo-server-sdk/-/expo-server-sdk-4.0.0.tgz", - "integrity": "sha512-zi83XtG2pqyP3gyn1JIRYkydo2i6HU3CYaWo/VvhZG/F29U+QIDv6LBEUsWf4ddZlVE7c9WN1N8Be49rHgO8OQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/expo-server-sdk/-/expo-server-sdk-5.0.0.tgz", + "integrity": "sha512-GEp1XYLU80iS/hdRo3c2n092E8TgTXcHSuw6Lw68dSoWaAgiLPI2R+e5hp5+hGF1TtJZOi2nxtJX63+XA3iz9g==", "requires": { - "node-fetch": "^2.6.0", "promise-limit": "^2.7.0", - "promise-retry": "^2.0.1" + "promise-retry": "^2.0.1", + "undici": "^7.2.0" }, "dependencies": { - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } + "undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==" } } }, @@ -29764,12 +29972,12 @@ "dev": true }, "fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.7.tgz", + "integrity": "sha512-JzVLro9NQv92pOM/jTCR6mHlJh2FGwtomH8ZQjhFj/R29P2Fnj38OgPJVtcvYw6SuKClhgYuwUZf5b3rd8u2mA==", "optional": true, "requires": { - "strnum": "^1.1.1" + "strnum": "^2.1.2" } }, "fastq": { @@ -29807,7 +30015,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "dev": true, + "devOptional": true, "requires": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -30022,9 +30230,9 @@ } }, "firebase-admin": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.6.0.tgz", - "integrity": "sha512-GdPA/t0+Cq8p1JnjFRBmxRxAGvF/kl2yfdhALl38PrRp325YxyQ5aNaHui0XmaKcKiGRFIJ/EgBNWFoDP0onjw==", + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.6.1.tgz", + "integrity": "sha512-Zgc6yPtmPxAZo+FoK6LMG6zpSEsoSK8ifIR+IqF4oWuC3uWZU40OjxgfLTSFcsRlj/k/wD66zNv2UiTRreCNSw==", "requires": { "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "^2.0.0", @@ -30108,7 +30316,7 @@ "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dev": true, + "devOptional": true, "requires": { "fetch-blob": "^3.1.2" } @@ -30251,20 +30459,6 @@ "uuid": "^9.0.1" }, "dependencies": { - "agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==" - }, - "https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "requires": { - "agent-base": "^7.1.2", - "debug": "4" - } - }, "node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -30299,6 +30493,115 @@ } } }, + "gcp-metadata": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "optional": true, + "peer": true, + "requires": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "optional": true, + "peer": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "optional": true, + "peer": true, + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + } + }, + "gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "optional": true, + "peer": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + } + }, + "glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "optional": true, + "peer": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "optional": true, + "peer": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "optional": true, + "peer": true + }, + "node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "optional": true, + "peer": true, + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, + "rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "optional": true, + "peer": true, + "requires": { + "glob": "^10.3.7" + } + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "optional": true, + "peer": true + } + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -30516,6 +30819,11 @@ "json-bigint": "^1.0.0" } }, + "google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==" + }, "jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -30597,9 +30905,11 @@ } }, "google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "optional": true, + "peer": true }, "gopd": { "version": "1.2.0", @@ -30872,17 +31182,6 @@ "requires": { "agent-base": "^7.1.0", "debug": "^4.3.4" - }, - "dependencies": { - "agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "requires": { - "debug": "^4.3.4" - } - } } }, "http2-wrapper": { @@ -30896,12 +31195,11 @@ } }, "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "optional": true, + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "requires": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" } }, @@ -31174,7 +31472,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "devOptional": true }, "issue-parser": { "version": "7.0.1", @@ -31321,7 +31619,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, + "devOptional": true, "requires": { "@isaacs/cliui": "^8.0.2", "@pkgjs/parseargs": "^0.11.0" @@ -32493,7 +32791,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true + "devOptional": true }, "minizlib": { "version": "2.1.2", @@ -32661,6 +32959,17 @@ "yargs": "^17.7.2" }, "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "debug": "4" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -32702,6 +33011,44 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + } + }, + "gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "mongodb": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", @@ -32713,6 +33060,45 @@ "mongodb-connection-string-url": "^3.0.2" } }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "optional": true, + "peer": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "optional": true, + "peer": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -32823,7 +33209,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "dev": true + "devOptional": true }, "node-emoji": { "version": "2.2.0", @@ -35044,7 +35430,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true + "devOptional": true }, "param-case": { "version": "3.0.4", @@ -35066,26 +35452,18 @@ } }, "parse": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/parse/-/parse-8.0.3.tgz", - "integrity": "sha512-WQPrnfnXy6/p25OFD6qOAVK9hIhhU882Nw1AW5RjAJbO2G7YqChJxBgL94aexsaTnP9ajVzjGISSQ+mESrkMIA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/parse/-/parse-8.2.0.tgz", + "integrity": "sha512-jSx4zIqCja6O2HKhkzZ6JTm4fBUQS6sQpvFCAsqzaU4XlEhoRLm9mM1tZeYhvxTVA6zymvj/EJZ4YDOXCTRbmA==", "requires": { "@babel/runtime": "7.28.6", - "@babel/runtime-corejs3": "7.28.6", + "@babel/runtime-corejs3": "7.29.0", "crypto-js": "4.2.0", "idb-keyval": "6.2.2", "react-native-crypto-js": "1.0.0", "ws": "8.19.0" }, "dependencies": { - "@babel/runtime-corejs3": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.6.tgz", - "integrity": "sha512-kz2fAQ5UzjV7X7D3ySxmj3vRq89dTpqOZWv76Z6pNPztkwb/0Yj1Mtx1xFrYj6mbIHysxtBot8J4o0JLCblcFw==", - "requires": { - "core-js-pure": "^3.43.0" - } - }, "ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -35166,7 +35544,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 + "devOptional": true }, "path-parse": { "version": "1.0.7", @@ -35178,7 +35556,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, + "devOptional": true, "requires": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -36493,7 +36871,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, + "devOptional": true, "requires": { "shebang-regex": "^3.0.0" } @@ -36502,7 +36880,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 + "devOptional": true }, "showdown": { "version": "2.1.0", @@ -36831,7 +37209,7 @@ "version": "npm:string-width@4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -36869,7 +37247,7 @@ "version": "npm:strip-ansi@6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "requires": { "ansi-regex": "^5.0.1" } @@ -36902,9 +37280,9 @@ "dev": true }, "strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", "optional": true }, "stubs": { @@ -37073,6 +37451,15 @@ "uuid": "^9.0.0" }, "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "requires": { + "debug": "4" + } + }, "http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -37084,6 +37471,16 @@ "debug": "4" } }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -37747,23 +38144,6 @@ "minimist": "^1.2.5" }, "dependencies": { - "agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "requires": { - "debug": "^4.3.4" - } - }, - "https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "requires": { - "agent-base": "^7.0.2", - "debug": "4" - } - }, "jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -37789,7 +38169,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "dev": true + "devOptional": true }, "webidl-conversions": { "version": "7.0.0", @@ -37829,7 +38209,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "devOptional": true, "requires": { "isexe": "^2.0.0" } @@ -37983,7 +38363,7 @@ "version": "npm:wrap-ansi@7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "devOptional": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -37994,7 +38374,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "requires": { "color-convert": "^2.0.1" } @@ -38003,7 +38383,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, + "devOptional": true, "requires": { "color-name": "~1.1.4" } @@ -38012,7 +38392,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true } } }, diff --git a/package.json b/package.json index 3861630153..9a67b990a6 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@graphql-tools/schema": "10.0.23", "@graphql-tools/utils": "10.8.6", "@parse/fs-files-adapter": "3.0.0", - "@parse/push-adapter": "8.2.0", + "@parse/push-adapter": "8.3.0", "bcryptjs": "3.0.3", "commander": "14.0.3", "cors": "2.8.6", @@ -48,7 +48,7 @@ "mongodb": "7.0.0", "mustache": "4.2.0", "otpauth": "9.4.0", - "parse": "8.0.3", + "parse": "8.2.0", "path-to-regexp": "8.3.0", "pg-monitor": "3.1.0", "pg-promise": "12.6.0", From c4c8f67304e7ef56e4af59a96447cf2c630bc2e2 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 20 Feb 2026 20:01:44 +0000 Subject: [PATCH 31/36] chore(release): 9.3.0-alpha.7 [skip ci] # [9.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.6...9.3.0-alpha.7) (2026-02-20) ### Features * Upgrade to parse 8.2.0, @parse/push-adapter 8.3.0 ([#10066](https://github.com/parse-community/parse-server/issues/10066)) ([8b5a14e](https://github.com/parse-community/parse-server/commit/8b5a14ecaf0b58b899651fb97d43e0e5d9be506d)) --- 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 57853d25ba..0e78d07294 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [9.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.6...9.3.0-alpha.7) (2026-02-20) + + +### Features + +* Upgrade to parse 8.2.0, @parse/push-adapter 8.3.0 ([#10066](https://github.com/parse-community/parse-server/issues/10066)) ([8b5a14e](https://github.com/parse-community/parse-server/commit/8b5a14ecaf0b58b899651fb97d43e0e5d9be506d)) + # [9.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.5...9.3.0-alpha.6) (2026-02-14) diff --git a/package-lock.json b/package-lock.json index a1a1ec7d55..1ecff90d2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.3.0-alpha.6", + "version": "9.3.0-alpha.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.3.0-alpha.6", + "version": "9.3.0-alpha.7", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 9a67b990a6..09ba3af5ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.3.0-alpha.6", + "version": "9.3.0-alpha.7", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 1a2521d930b855845aa13fde700b2e8170ff65a1 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 21 Feb 2026 01:20:12 +0000 Subject: [PATCH 32/36] fix: Incorrect dependency chain of `Parse` uses browser build instead of Node build (#10067) --- package-lock.json | 14 +++++++------- package.json | 2 +- src/Controllers/FilesController.js | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ecff90d2d..b0f2c5dfe9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@graphql-tools/schema": "10.0.23", "@graphql-tools/utils": "10.8.6", "@parse/fs-files-adapter": "3.0.0", - "@parse/push-adapter": "8.3.0", + "@parse/push-adapter": "8.3.1", "bcryptjs": "3.0.3", "commander": "14.0.3", "cors": "2.8.6", @@ -4155,9 +4155,9 @@ } }, "node_modules/@parse/push-adapter": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-8.3.0.tgz", - "integrity": "sha512-snHVH0j5pqleU3uBUjHCzTR1ex8FFnAaDbKX3Bz+L6w2mLT2Zio2e7fxxrDCorluVBVJvorSsSjgZQsWCXL+Cg==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-8.3.1.tgz", + "integrity": "sha512-UOkL0bTOtUx4R866XtBwGGYvkNLQrQPrPWC4uzpbd9vR8tbfYIQQvBDT7eg58GryLb4EG+NOP8enm/VkhbwMVw==", "license": "MIT", "dependencies": { "@parse/node-apn": "7.1.0", @@ -25584,9 +25584,9 @@ } }, "@parse/push-adapter": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-8.3.0.tgz", - "integrity": "sha512-snHVH0j5pqleU3uBUjHCzTR1ex8FFnAaDbKX3Bz+L6w2mLT2Zio2e7fxxrDCorluVBVJvorSsSjgZQsWCXL+Cg==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-8.3.1.tgz", + "integrity": "sha512-UOkL0bTOtUx4R866XtBwGGYvkNLQrQPrPWC4uzpbd9vR8tbfYIQQvBDT7eg58GryLb4EG+NOP8enm/VkhbwMVw==", "requires": { "@parse/node-apn": "7.1.0", "expo-server-sdk": "5.0.0", diff --git a/package.json b/package.json index 09ba3af5ff..ca3fce1f36 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@graphql-tools/schema": "10.0.23", "@graphql-tools/utils": "10.8.6", "@parse/fs-files-adapter": "3.0.0", - "@parse/push-adapter": "8.3.0", + "@parse/push-adapter": "8.3.1", "bcryptjs": "3.0.3", "commander": "14.0.3", "cors": "2.8.6", diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index a88c527b00..39db972fe0 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -3,7 +3,7 @@ import { randomHexString } from '../cryptoUtils'; import AdaptableController from './AdaptableController'; import { validateFilename, FilesAdapter } from '../Adapters/Files/FilesAdapter'; import path from 'path'; -const Parse = require('parse').Parse; +const Parse = require('parse/node').Parse; const legacyFilesRegex = new RegExp( '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}-.*' From 8a8006c30b1a8516e59c71b26f803f5bfc60bc65 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 21 Feb 2026 01:21:06 +0000 Subject: [PATCH 33/36] chore(release): 9.3.0-alpha.8 [skip ci] # [9.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.7...9.3.0-alpha.8) (2026-02-21) ### Bug Fixes * Incorrect dependency chain of `Parse` uses browser build instead of Node build ([#10067](https://github.com/parse-community/parse-server/issues/10067)) ([1a2521d](https://github.com/parse-community/parse-server/commit/1a2521d930b855845aa13fde700b2e8170ff65a1)) --- 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 0e78d07294..070c3cd849 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [9.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.7...9.3.0-alpha.8) (2026-02-21) + + +### Bug Fixes + +* Incorrect dependency chain of `Parse` uses browser build instead of Node build ([#10067](https://github.com/parse-community/parse-server/issues/10067)) ([1a2521d](https://github.com/parse-community/parse-server/commit/1a2521d930b855845aa13fde700b2e8170ff65a1)) + # [9.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.6...9.3.0-alpha.7) (2026-02-20) diff --git a/package-lock.json b/package-lock.json index b0f2c5dfe9..b938935421 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.3.0-alpha.7", + "version": "9.3.0-alpha.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.3.0-alpha.7", + "version": "9.3.0-alpha.8", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index ca3fce1f36..63eb01a24c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.3.0-alpha.7", + "version": "9.3.0-alpha.8", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From f0feb48d0fb697a161693721eadd09d740336283 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 21 Feb 2026 03:31:16 +0000 Subject: [PATCH 34/36] feat: Add support for streaming file upload via `Buffer`, `Readable`, `ReadableStream` (#10065) --- spec/GridFSBucketStorageAdapter.spec.js | 25 ++ spec/ParseFile.spec.js | 275 ++++++++++++++++++++++ spec/Utils.spec.js | 75 ++++++ src/Adapters/Files/FilesAdapter.js | 12 +- src/Adapters/Files/GridFSBucketAdapter.js | 30 +++ src/Controllers/AdaptableController.js | 5 + src/Controllers/FilesController.js | 10 + src/Routers/FilesRouter.js | 256 +++++++++++++++++++- src/Utils.js | 34 +++ 9 files changed, 708 insertions(+), 14 deletions(-) diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js index 57f94c2dab..6a274125bc 100644 --- a/spec/GridFSBucketStorageAdapter.spec.js +++ b/spec/GridFSBucketStorageAdapter.spec.js @@ -476,6 +476,31 @@ describe_only_db('mongo')('GridFSBucket', () => { } }); + it('reports supportsStreaming as true', () => { + const gfsAdapter = new GridFSBucketAdapter(databaseURI); + expect(gfsAdapter.supportsStreaming).toBe(true); + }); + + it('creates file from Readable stream', async () => { + const { Readable } = require('stream'); + const gfsAdapter = new GridFSBucketAdapter(databaseURI); + const data = Buffer.from('streamed file content'); + const stream = Readable.from(data); + await gfsAdapter.createFile('streamFile.txt', stream); + const result = await gfsAdapter.getFileData('streamFile.txt'); + expect(result.toString('utf8')).toBe('streamed file content'); + }); + + it('creates encrypted file from Readable stream (buffers for encryption)', async () => { + const { Readable } = require('stream'); + const gfsAdapter = new GridFSBucketAdapter(databaseURI, {}, 'test-encryption-key'); + const data = Buffer.from('encrypted streamed content'); + const stream = Readable.from(data); + await gfsAdapter.createFile('encryptedStream.txt', stream); + const result = await gfsAdapter.getFileData('encryptedStream.txt'); + expect(result.toString('utf8')).toBe('encrypted streamed content'); + }); + describe('MongoDB Client Metadata', () => { it('should not pass metadata to MongoClient by default', async () => { const gfsAdapter = new GridFSBucketAdapter(databaseURI); diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 46496a0768..da69edc416 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -1878,4 +1878,279 @@ describe('Parse.File testing', () => { ).toBeRejectedWith(jasmine.objectContaining({ status: 400 })); }); }); + + describe('streaming binary uploads', () => { + afterEach(() => { + Parse.Cloud._removeAllHooks(); + }); + + describe('createSizeLimitedStream', () => { + const { createSizeLimitedStream } = require('../lib/Routers/FilesRouter'); + const { Readable } = require('stream'); + + it('passes data through when under limit', async () => { + const input = Readable.from(Buffer.from('hello')); + const limited = createSizeLimitedStream(input, 100); + const chunks = []; + for await (const chunk of limited) { + chunks.push(chunk); + } + expect(Buffer.concat(chunks).toString()).toBe('hello'); + }); + + it('destroys stream when data exceeds limit', async () => { + const input = Readable.from(Buffer.from('hello world, this is too long')); + const limited = createSizeLimitedStream(input, 5); + const chunks = []; + try { + for await (const chunk of limited) { + chunks.push(chunk); + } + fail('should have thrown'); + } catch (e) { + expect(e.message).toContain('exceeds'); + } + }); + + }); + + it('streams binary upload with X-Parse-Upload-Mode header', async () => { + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Upload-Mode': 'stream', + }; + let response; + try { + response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/files/stream-test.txt', + body: 'streaming file content', + }); + } catch (e) { + fail('Request failed: status=' + e.status + ' text=' + e.text + ' data=' + JSON.stringify(e.data)); + return; + } + const b = response.data; + expect(b.name).toMatch(/_stream-test.txt$/); + expect(b.url).toMatch(/stream-test\.txt$/); + const getResponse = await request({ url: b.url }); + expect(getResponse.text).toEqual('streaming file content'); + }); + + it('infers content type from extension when Content-Type header is missing', async () => { + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Upload-Mode': 'stream', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/files/inferred.txt', + body: 'inferred content type', + }); + const b = response.data; + expect(b.name).toMatch(/_inferred.txt$/); + const getResponse = await request({ url: b.url }); + expect(getResponse.text).toEqual('inferred content type'); + }); + + it('uses buffered path without X-Parse-Upload-Mode header', async () => { + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/files/buffered-test.txt', + body: 'buffered file content', + }); + const b = response.data; + expect(b.name).toMatch(/_buffered-test.txt$/); + const getResponse = await request({ url: b.url }); + expect(getResponse.text).toEqual('buffered file content'); + }); + + it('rejects streaming upload exceeding size limit', async () => { + await reconfigureServer({ maxUploadSize: '10b' }); + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Upload-Mode': 'stream', + }; + try { + await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/files/big-file.txt', + body: 'this content is definitely longer than 10 bytes', + }); + fail('should have thrown'); + } catch (response) { + expect(response.data.code).toBe(Parse.Error.FILE_SAVE_ERROR); + expect(response.data.error).toContain('exceeds'); + } + }); + + it('rejects streaming upload with Content-Length exceeding limit', async () => { + await reconfigureServer({ maxUploadSize: '10b' }); + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Upload-Mode': 'stream', + 'Content-Length': '99999', + }; + try { + await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/files/big-file.txt', + body: 'hi', + }); + fail('should have thrown'); + } catch (response) { + expect(response.data.code).toBe(Parse.Error.FILE_SAVE_ERROR); + expect(response.data.error).toContain('exceeds'); + } + }); + + it('fires beforeSave trigger with request.stream = true on streaming upload', async () => { + let receivedStream; + let receivedData; + Parse.Cloud.beforeSave(Parse.File, (request) => { + receivedStream = request.stream; + receivedData = request.file._data; + request.file.addMetadata('source', 'stream'); + request.file.addTag('env', 'test'); + }); + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Upload-Mode': 'stream', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/files/trigger-test.txt', + body: 'trigger test content', + }); + expect(response.data.name).toMatch(/_trigger-test.txt$/); + expect(receivedStream).toBe(true); + expect(receivedData).toBeFalsy(); + const getResponse = await request({ url: response.data.url }); + expect(getResponse.text).toEqual('trigger test content'); + }); + + it('rejects streaming upload when beforeSave trigger throws', async () => { + Parse.Cloud.beforeSave(Parse.File, () => { + throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Upload rejected'); + }); + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Upload-Mode': 'stream', + }; + try { + await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/files/rejected.txt', + body: 'rejected content', + }); + fail('should have thrown'); + } catch (response) { + expect(response.data.code).toBe(Parse.Error.SCRIPT_FAILED); + expect(response.data.error).toBe('Upload rejected'); + } + }); + + it('skips save when beforeSave trigger returns Parse.File with URL on streaming upload', async () => { + Parse.Cloud.beforeSave(Parse.File, () => { + return Parse.File.fromJSON({ + __type: 'File', + name: 'existing.txt', + url: 'http://example.com/existing.txt', + }); + }); + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Upload-Mode': 'stream', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/files/skip-save.txt', + body: 'should not be saved', + }); + expect(response.data.url).toBe('http://example.com/existing.txt'); + expect(response.data.name).toBe('existing.txt'); + }); + + it('fires afterSave trigger with request.stream = true on streaming upload', async () => { + let afterSaveStream; + let afterSaveData; + let afterSaveUrl; + Parse.Cloud.afterSave(Parse.File, (request) => { + afterSaveStream = request.stream; + afterSaveData = request.file._data; + afterSaveUrl = request.file._url; + }); + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Upload-Mode': 'stream', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/files/after-save.txt', + body: 'after save content', + }); + expect(response.data.name).toMatch(/_after-save.txt$/); + expect(afterSaveStream).toBe(true); + expect(afterSaveData).toBeFalsy(); + expect(afterSaveUrl).toBeTruthy(); + }); + + it('verifies FilesAdapter default supportsStreaming is false', () => { + const { FilesAdapter } = require('../lib/Adapters/Files/FilesAdapter'); + const adapter = new FilesAdapter(); + expect(adapter.supportsStreaming).toBe(false); + }); + + it('legacy JSON-wrapped upload still works', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + fileExtensions: ['*'], + }, + }); + const response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/files/legacy.txt', + body: JSON.stringify({ + _ApplicationId: 'test', + _JavaScriptKey: 'test', + _ContentType: 'text/plain', + base64: Buffer.from('legacy content').toString('base64'), + }), + }); + const b = response.data; + expect(b.name).toMatch(/_legacy.txt$/); + const getResponse = await request({ url: b.url }); + expect(getResponse.text).toEqual('legacy content'); + }); + }); }); diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js index a473064376..853ddc9b3c 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -175,6 +175,81 @@ describe('Utils', () => { }); }); + describe('parseSizeToBytes', () => { + it('parses megabyte string', () => { + expect(Utils.parseSizeToBytes('20mb')).toBe(20 * 1024 * 1024); + }); + + it('parses Mb string (case-insensitive)', () => { + expect(Utils.parseSizeToBytes('20Mb')).toBe(20 * 1024 * 1024); + }); + + it('parses kilobyte string', () => { + expect(Utils.parseSizeToBytes('512kb')).toBe(512 * 1024); + }); + + it('parses gigabyte string', () => { + expect(Utils.parseSizeToBytes('1gb')).toBe(1 * 1024 * 1024 * 1024); + }); + + it('parses bytes suffix', () => { + expect(Utils.parseSizeToBytes('100b')).toBe(100); + }); + + it('parses plain number as bytes', () => { + expect(Utils.parseSizeToBytes(1048576)).toBe(1048576); + }); + + it('parses numeric string as bytes', () => { + expect(Utils.parseSizeToBytes('1048576')).toBe(1048576); + }); + + it('parses decimal value and floors result', () => { + expect(Utils.parseSizeToBytes('1.5mb')).toBe(Math.floor(1.5 * 1024 * 1024)); + }); + + it('trims whitespace around value', () => { + expect(Utils.parseSizeToBytes(' 20mb ')).toBe(20 * 1024 * 1024); + }); + + it('allows whitespace between number and unit', () => { + expect(Utils.parseSizeToBytes('20 mb')).toBe(20 * 1024 * 1024); + }); + + it('parses zero', () => { + expect(Utils.parseSizeToBytes('0')).toBe(0); + expect(Utils.parseSizeToBytes(0)).toBe(0); + }); + + it('throws on invalid string', () => { + expect(() => Utils.parseSizeToBytes('abc')).toThrow(); + }); + + it('throws on negative value', () => { + expect(() => Utils.parseSizeToBytes('-5mb')).toThrow(); + }); + + it('throws on empty string', () => { + expect(() => Utils.parseSizeToBytes('')).toThrow(); + }); + + it('throws on unsupported unit', () => { + expect(() => Utils.parseSizeToBytes('10tb')).toThrow(); + }); + + it('throws on NaN', () => { + expect(() => Utils.parseSizeToBytes(NaN)).toThrow(); + }); + + it('throws on Infinity', () => { + expect(() => Utils.parseSizeToBytes(Infinity)).toThrow(); + }); + + it('throws on negative number', () => { + expect(() => Utils.parseSizeToBytes(-1)).toThrow(); + }); + }); + describe('createSanitizedError', () => { it('should return "Permission denied" when enableSanitizedErrorResponse is true', () => { const config = { enableSanitizedErrorResponse: true }; diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js index 0e9b555853..0ada581069 100644 --- a/src/Adapters/Files/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -26,7 +26,7 @@ export class FilesAdapter { /** Responsible for storing the file in order to be retrieved later by its filename * * @param {string} filename - the filename to save - * @param {*} data - the buffer of data from the file + * @param {Buffer|import('stream').Readable} data - the file data as a Buffer, or a Readable stream if the adapter supports streaming (see supportsStreaming) * @param {string} contentType - the supposed contentType * @discussion the contentType can be undefined if the controller was not able to determine it * @param {object} options - (Optional) options to be passed to file adapter (S3 File Adapter Only) @@ -38,6 +38,16 @@ export class FilesAdapter { */ createFile(filename: string, data, contentType: string, options: Object): Promise {} + /** Whether this adapter supports receiving Readable streams in createFile(). + * If false (default), streams are buffered to a Buffer before being passed. + * Override and return true to receive Readable streams directly. + * + * @return {boolean} + */ + get supportsStreaming() { + return false; + } + /** Responsible for deleting the specified file * * @param {string} filename - the filename to delete diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 8e1a849030..18c4eaa3d6 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -45,6 +45,10 @@ export class GridFSBucketAdapter extends FilesAdapter { this._mongoOptions = _mongoOptions; } + get supportsStreaming() { + return true; + } + _connect() { if (!this._connectionPromise) { // Only use driverInfo if clientMetadata option is set @@ -77,6 +81,32 @@ export class GridFSBucketAdapter extends FilesAdapter { const stream = await bucket.openUploadStream(filename, { metadata: options.metadata, }); + + // If data is a stream and encryption is enabled, buffer first + // (AES-256-GCM needs complete data for format: [encrypted][IV][authTag]) + if (typeof data?.pipe === 'function' && this._encryptionKey !== null) { + data = await new Promise((resolve, reject) => { + const chunks = []; + data.on('data', chunk => chunks.push(chunk)); + data.on('end', () => resolve(Buffer.concat(chunks))); + data.on('error', reject); + }); + } + + if (typeof data?.pipe === 'function') { + // Pipe readable stream directly into GridFS upload stream + return new Promise((resolve, reject) => { + data.pipe(stream); + stream.on('finish', resolve); + stream.on('error', reject); + data.on('error', (err) => { + stream.destroy(err); + reject(err); + }); + }); + } + + // Buffer path (existing behavior) if (this._encryptionKey !== null) { try { const iv = crypto.randomBytes(16); diff --git a/src/Controllers/AdaptableController.js b/src/Controllers/AdaptableController.js index 15551a6e38..610b48b3fd 100644 --- a/src/Controllers/AdaptableController.js +++ b/src/Controllers/AdaptableController.js @@ -48,6 +48,11 @@ export class AdaptableController { // Makes sure the prototype matches const mismatches = Object.getOwnPropertyNames(Type.prototype).reduce((obj, key) => { + // Skip getters — they provide optional defaults that adapters don't need to implement + const descriptor = Object.getOwnPropertyDescriptor(Type.prototype, key); + if (descriptor && typeof descriptor.get === 'function') { + return obj; + } const adapterType = typeof adapter[key]; const expectedType = typeof Type.prototype[key]; if (adapterType !== expectedType) { diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 39db972fe0..e21ea7c1a3 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -29,6 +29,16 @@ export class FilesController extends AdaptableController { filename = randomHexString(32) + '_' + filename; } + // Fallback: buffer stream for adapters that don't support streaming + if (typeof data?.pipe === 'function' && !this.adapter.supportsStreaming) { + data = await new Promise((resolve, reject) => { + const chunks = []; + data.on('data', chunk => chunks.push(chunk)); + data.on('end', () => resolve(Buffer.concat(chunks))); + data.on('error', reject); + }); + } + const location = await this.adapter.getFileLocation(config, filename); await this.adapter.createFile(filename, data, contentType, options); return { diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index f0bb483d7b..f53e1c9d87 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -5,6 +5,79 @@ import Config from '../Config'; import logger from '../logger'; const triggers = require('../triggers'); const Utils = require('../Utils'); +import { Readable } from 'stream'; + +/** + * Wraps a readable stream in a Readable that enforces a byte size limit. + * Data flow is lazy: the source is not read until a consumer starts reading + * from the returned stream (via pipe or 'data' listener). This ensures the + * consumer's error listener is attached before any data (or error) is emitted. + */ +export function createSizeLimitedStream(source, maxBytes) { + let totalBytes = 0; + let started = false; + let sourceEnded = false; + let onData, onEnd, onError; + + const output = new Readable({ + read() { + if (!started) { + started = true; + + onData = (chunk) => { + totalBytes += chunk.length; + if (totalBytes > maxBytes) { + output.destroy( + new Parse.Error( + Parse.Error.FILE_SAVE_ERROR, + `File size exceeds maximum allowed: ${maxBytes} bytes.` + ) + ); + return; + } + if (!output.push(chunk)) { + source.pause(); + } + }; + + onEnd = () => { + sourceEnded = true; + output.push(null); + }; + + onError = (err) => output.destroy(err); + + source.on('data', onData); + source.on('end', onEnd); + source.on('error', onError); + } + + // Resume source in case it was paused due to backpressure + if (!sourceEnded) { + source.resume(); + } + }, + destroy(err, callback) { + if (onData) { + source.removeListener('data', onData); + } + if (onEnd) { + source.removeListener('end', onEnd); + } + if (onError) { + source.removeListener('error', onError); + } + // Suppress errors emitted during drain (e.g. client disconnect) + source.on('error', () => {}); + if (!sourceEnded) { + source.resume(); + } + callback(err); + } + }); + + return output; +} export class FilesRouter { expressRouter({ maxUploadSize = '20Mb' } = {}) { @@ -18,15 +91,10 @@ export class FilesRouter { router.post( '/files/:filename', - express.raw({ - type: () => { - return true; - }, - limit: maxUploadSize, - }), // Allow uploads without Content-Type, or with any Content-Type. + this._bodyParsingMiddleware(maxUploadSize), Middlewares.handleParseHeaders, Middlewares.handleParseSession, - this.createHandler + this.createHandler.bind(this) ); router.delete( @@ -111,6 +179,20 @@ export class FilesRouter { } } + _bodyParsingMiddleware(maxUploadSize) { + const rawParser = express.raw({ + type: () => true, + limit: maxUploadSize, + }); + return (req, res, next) => { + if (req.get('X-Parse-Upload-Mode') === 'stream') { + req._maxUploadSizeBytes = Utils.parseSizeToBytes(maxUploadSize); + return next(); + } + return rawParser(req, res, next); + }; + } + async createHandler(req, res, next) { const config = req.config; const user = req.auth.user; @@ -139,11 +221,6 @@ export class FilesRouter { const { filename } = req.params; const contentType = req.get('Content-type'); - if (!req.body || !req.body.length) { - next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.')); - return; - } - const error = filesController.validateFilename(filename); if (error) { next(error); @@ -182,6 +259,24 @@ export class FilesRouter { } } + // Dispatch to the appropriate handler based on whether the body was buffered + if (req.body instanceof Buffer) { + return this._handleBufferedUpload(req, res, next); + } + return this._handleStreamUpload(req, res, next); + } + + async _handleBufferedUpload(req, res, next) { + const config = req.config; + const filesController = config.filesController; + const { filename } = req.params; + const contentType = req.get('Content-type'); + + if (!req.body || !req.body.length) { + next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.')); + return; + } + const base64 = req.body.toString('base64'); const file = new Parse.File(filename, { base64 }, contentType); const { metadata = {}, tags = {} } = req.fileData || {}; @@ -221,7 +316,12 @@ export class FilesRouter { // if the file returned by the trigger has already been saved skip saving anything if (!saveResult) { // update fileSize - const bufferData = Buffer.from(fileObject.file._data, 'base64'); + let bufferData; + if (fileObject.file._source?.format === 'buffer') { + bufferData = fileObject.file._source.buffer; + } else { + bufferData = Buffer.from(fileObject.file._data, 'base64'); + } fileObject.fileSize = Buffer.byteLength(bufferData); // prepare file options const fileOptions = { @@ -265,6 +365,136 @@ export class FilesRouter { } } + async _handleStreamUpload(req, res, next) { + const config = req.config; + const filesController = config.filesController; + const { filename } = req.params; + let contentType = req.get('Content-Type'); + const maxBytes = req._maxUploadSizeBytes; + let stream; + + try { + // Early rejection via Content-Length header + const contentLength = req.get('Content-Length'); + if (contentLength && parseInt(contentLength, 10) > maxBytes) { + req.resume(); + next(new Parse.Error( + Parse.Error.FILE_SAVE_ERROR, + `File size exceeds maximum allowed: ${maxBytes} bytes.` + )); + return; + } + + const mime = (await import('mime')).default; + + // Infer content type from extension or add extension from content type + const hasExtension = filename && filename.includes('.'); + if (hasExtension && !contentType) { + contentType = mime.getType(filename); + } else if (!hasExtension && contentType) { + // extension will be added by filesController.createFile + } + + // Create size-limited stream wrapping the request + stream = createSizeLimitedStream(req, maxBytes); + + // Build a Parse.File with no _data (streaming mode) + const file = new Parse.File(filename, { base64: '' }, contentType); + const { metadata = {}, tags = {} } = req.fileData || {}; + + // Validate metadata and tags for prohibited keywords + try { + Utils.checkProhibitedKeywords(config, metadata); + Utils.checkProhibitedKeywords(config, tags); + } catch (error) { + stream.destroy(); + next(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error)); + return; + } + + file.setTags(tags); + file.setMetadata(metadata); + + const fileSize = req.get('Content-Length') + ? parseInt(req.get('Content-Length'), 10) + : null; + const fileObject = { file, fileSize, stream: true }; + + // Run beforeSaveFile trigger + const triggerResult = await triggers.maybeRunFileTrigger( + triggers.Types.beforeSave, + fileObject, + config, + req.auth + ); + + let saveResult; + // If a new ParseFile is returned, check if it's an already saved file + if (triggerResult instanceof Parse.File) { + fileObject.file = triggerResult; + if (triggerResult.url()) { + fileObject.fileSize = null; + saveResult = { + url: triggerResult.url(), + name: triggerResult._name, + }; + // Destroy stream to remove listeners and drain request + stream.destroy(); + } + } + + // If the file returned by the trigger has already been saved, skip saving + if (!saveResult) { + // Prepare file options + const fileOptions = { + metadata: fileObject.file._metadata, + }; + const fileTags = + Object.keys(fileObject.file._tags).length > 0 ? { tags: fileObject.file._tags } : {}; + Object.assign(fileOptions, fileTags); + + // Pass stream directly to filesController — it will buffer if adapter doesn't support streaming + const sourceType = fileObject.file._source?.type || contentType; + const createFileResult = await filesController.createFile( + config, + fileObject.file._name, + stream, + sourceType, + fileOptions + ); + + // Update file with new data + fileObject.file._name = createFileResult.name; + fileObject.file._url = createFileResult.url; + fileObject.file._requestTask = null; + fileObject.file._previousSave = Promise.resolve(fileObject.file); + saveResult = { + url: createFileResult.url, + name: createFileResult.name, + }; + } + + // Run afterSaveFile trigger + await triggers.maybeRunFileTrigger(triggers.Types.afterSave, fileObject, config, req.auth); + res.status(201); + res.set('Location', saveResult.url); + res.json(saveResult); + } catch (e) { + // Destroy stream to remove listeners and drain request, or resume directly + if (stream) { + stream.destroy(); + } else { + req.resume(); + } + logger.error('Error creating a file: ', e); + const error = triggers.resolveError(e, { + code: Parse.Error.FILE_SAVE_ERROR, + message: `Could not store file: ${filename}.`, + }); + next(error); + } + } + async deleteHandler(req, res, next) { try { const { filesController } = req.config; diff --git a/src/Utils.js b/src/Utils.js index 0eca833552..1e072725d7 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -469,6 +469,40 @@ class Utils { } return current; } + + /** + * Parses a human-readable size string into a byte count. + * @param {number | string} size - A number (floored to an integer), a numeric string + * (treated as bytes), or a string with a unit suffix: `b`, `kb`, `mb`, `gb` + * (case-insensitive). Examples: `'20mb'`, `'512kb'`, `'1.5gb'`, `1048576`. + * @returns {number} The size in bytes, floored to the nearest integer. + * @throws {Error} If the string does not match the expected format. + */ + static parseSizeToBytes(size) { + if (typeof size === 'number') { + if (!Number.isFinite(size) || size < 0) { + throw new Error(`Invalid size value: ${size}`); + } + return Math.floor(size); + } + const str = String(size).trim().toLowerCase(); + const match = str.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/); + if (!match) { + throw new Error(`Invalid size value: ${size}`); + } + const num = parseFloat(match[1]); + const unit = match[2]; + switch (unit) { + case 'kb': + return Math.floor(num * 1024); + case 'mb': + return Math.floor(num * 1024 * 1024); + case 'gb': + return Math.floor(num * 1024 * 1024 * 1024); + default: + return Math.floor(num); + } + } } module.exports = Utils; From c7df72341bcdf6455ac49a45e422df1251be1faa Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 21 Feb 2026 03:34:02 +0000 Subject: [PATCH 35/36] chore(release): 9.3.0-alpha.9 [skip ci] # [9.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.8...9.3.0-alpha.9) (2026-02-21) ### Features * Add support for streaming file upload via `Buffer`, `Readable`, `ReadableStream` ([#10065](https://github.com/parse-community/parse-server/issues/10065)) ([f0feb48](https://github.com/parse-community/parse-server/commit/f0feb48d0fb697a161693721eadd09d740336283)) --- 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 070c3cd849..1ea16fd25b 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [9.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.8...9.3.0-alpha.9) (2026-02-21) + + +### Features + +* Add support for streaming file upload via `Buffer`, `Readable`, `ReadableStream` ([#10065](https://github.com/parse-community/parse-server/issues/10065)) ([f0feb48](https://github.com/parse-community/parse-server/commit/f0feb48d0fb697a161693721eadd09d740336283)) + # [9.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/9.3.0-alpha.7...9.3.0-alpha.8) (2026-02-21) diff --git a/package-lock.json b/package-lock.json index b938935421..f075ff8510 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.3.0-alpha.8", + "version": "9.3.0-alpha.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.3.0-alpha.8", + "version": "9.3.0-alpha.9", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 63eb01a24c..08427f4f51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.3.0-alpha.8", + "version": "9.3.0-alpha.9", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 66762cd136d745e5c3fb2f75b80fefafa4592eeb Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sat, 21 Feb 2026 15:19:44 +0000 Subject: [PATCH 36/36] empty commit to trigger CI