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/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/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 89be5e840a..1ea16fd25b 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,80 @@ +# [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) + + +### 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) + + +### 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) + + +### 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) + + +### 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) + + +### 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) + + +### 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) + + +### 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) + + +### 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) + + +### 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) + + +### 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/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 c46e23133c..f075ff8510 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.2.0", + "version": "9.3.0-alpha.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.2.0", + "version": "9.3.0-alpha.9", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -16,13 +16,13 @@ "@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.1", "bcryptjs": "3.0.3", - "commander": "14.0.2", + "commander": "14.0.3", "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", @@ -38,9 +38,9 @@ "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.0.0", + "pg-monitor": "3.1.0", "pg-promise": "12.6.0", "pluralize": "8.0.0", "punycode": "2.3.1", @@ -58,13 +58,13 @@ "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.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", + "@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", @@ -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", @@ -81,9 +81,9 @@ "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", + "globals": "17.3.0", "graphql-tag": "2.12.6", "jasmine": "5.7.1", "jasmine-spec-reporter": "7.0.0", @@ -98,11 +98,11 @@ "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": "5.9.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" @@ -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": { @@ -497,9 +498,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 +521,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 +593,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" @@ -802,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" } @@ -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" @@ -1050,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" @@ -1438,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" @@ -2177,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", @@ -2192,17 +2202,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 +2220,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", @@ -2604,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": { @@ -2616,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", @@ -2714,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": { @@ -3044,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", @@ -3061,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" }, @@ -3073,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" }, @@ -3085,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", @@ -3108,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" }, @@ -3123,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", @@ -4087,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" }, @@ -4101,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.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.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": { @@ -4649,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", @@ -4945,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", @@ -5445,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", @@ -5520,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", @@ -5814,9 +5817,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", @@ -7151,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": { @@ -7531,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", @@ -7568,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": "*" } @@ -8555,9 +8556,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" } @@ -8722,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": { @@ -8814,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", @@ -8860,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" } @@ -9470,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", @@ -9916,14 +9917,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": { @@ -10244,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": { @@ -10342,10 +10309,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" }, @@ -10378,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", @@ -10463,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", @@ -10475,7 +10445,7 @@ "license": "MIT", "optional": true, "dependencies": { - "strnum": "^1.1.1" + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -10521,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", @@ -10844,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", @@ -10971,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" }, @@ -11178,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", @@ -11255,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", @@ -11463,11 +11575,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" }, @@ -11550,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", @@ -11565,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" @@ -11655,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" } @@ -11807,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" @@ -12065,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", @@ -12092,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": { @@ -12274,6 +12386,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", @@ -12503,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", @@ -12691,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" }, @@ -12933,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" } @@ -14304,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" } @@ -14543,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", @@ -14599,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", @@ -14645,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", @@ -14813,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", @@ -18098,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", @@ -18123,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", @@ -18166,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", @@ -18263,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" } @@ -18278,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" @@ -18385,15 +18613,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": { @@ -18772,15 +18999,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": { @@ -18947,9 +19178,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" }, @@ -20197,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" }, @@ -20209,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" } @@ -20641,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", @@ -20690,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" }, @@ -20739,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", @@ -20988,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", @@ -21003,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", @@ -21509,11 +21767,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" @@ -21698,6 +21955,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", @@ -21974,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", @@ -22020,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" } @@ -22081,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" }, @@ -22245,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", @@ -22262,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" }, @@ -22277,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" }, @@ -22289,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", @@ -22389,16 +22632,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": { @@ -22558,37 +22803,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": { @@ -22819,9 +23065,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 +23082,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 +23138,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" @@ -23051,9 +23297,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": { @@ -23128,12 +23374,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": { @@ -23206,12 +23452,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": { @@ -23436,13 +23682,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": { @@ -23901,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", @@ -23913,24 +24167,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", @@ -24216,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", @@ -24227,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", @@ -24290,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", @@ -24513,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", @@ -24527,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", @@ -24556,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" } @@ -24565,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", @@ -25281,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.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.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" } }, @@ -25716,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", @@ -25912,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", @@ -26257,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", @@ -26305,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", @@ -26486,9 +26740,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", @@ -27402,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", @@ -27691,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", @@ -27710,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", @@ -28403,9 +28653,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", @@ -28526,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", @@ -28578,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", @@ -28611,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", @@ -29056,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", @@ -29454,9 +29704,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": {} }, @@ -29587,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==" } } }, @@ -29676,10 +29904,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", @@ -29742,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": { @@ -29785,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" @@ -30000,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", @@ -30086,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" } @@ -30229,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", @@ -30277,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", @@ -30435,9 +30760,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": { @@ -30494,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", @@ -30575,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", @@ -30850,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": { @@ -30874,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" } }, @@ -30994,6 +31314,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", @@ -31147,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", @@ -31294,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" @@ -32466,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", @@ -32634,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", @@ -32675,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", @@ -32686,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", @@ -32796,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", @@ -35017,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", @@ -35039,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", @@ -35139,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", @@ -35151,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" @@ -35216,11 +35621,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": { @@ -35479,9 +35884,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": { @@ -35614,9 +36019,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" } @@ -36466,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" } @@ -36475,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", @@ -36804,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", @@ -36842,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" } @@ -36875,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": { @@ -37046,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", @@ -37057,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", @@ -37406,9 +37830,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": { @@ -37524,6 +37948,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", @@ -37714,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", @@ -37756,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", @@ -37796,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" } @@ -37950,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", @@ -37961,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" } @@ -37970,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" } @@ -37979,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 } } }, @@ -38030,9 +38443,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 f01b480c2d..08427f4f51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.2.0", + "version": "9.3.0-alpha.9", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { @@ -10,7 +10,7 @@ "files": [ "bin/", "lib/", - "public_html/", + "public/", "views/", "LICENSE", "NOTICE", @@ -26,13 +26,13 @@ "@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.1", "bcryptjs": "3.0.3", - "commander": "14.0.2", + "commander": "14.0.3", "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", @@ -48,9 +48,9 @@ "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.0.0", + "pg-monitor": "3.1.0", "pg-promise": "12.6.0", "pluralize": "8.0.0", "punycode": "2.3.1", @@ -65,13 +65,13 @@ "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.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", + "@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", @@ -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", @@ -88,9 +88,9 @@ "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", + "globals": "17.3.0", "graphql-tag": "2.12.6", "jasmine": "5.7.1", "jasmine-spec-reporter": "7.0.0", @@ -105,11 +105,11 @@ "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": "5.9.3", "typescript-eslint": "8.53.1", - "yaml": "2.8.0" + "yaml": "2.8.2" }, "scripts": { "ci:check": "node ./ci/ciCheck.js", 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/AuthenticationAdaptersV2.spec.js b/spec/AuthenticationAdaptersV2.spec.js index 7301ab54c1..d8c646382c 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,280 @@ describe('Auth Adapter features', () => { await user.fetch({ useMasterKey: true }); 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: { + 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/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/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/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/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/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 009254dfcc..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 ); @@ -1181,6 +1179,91 @@ 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('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/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/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 5c1c3c99e7..da69edc416 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,504 @@ 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 })); + }); + }); + + 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/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/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/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/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/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/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/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/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 d8bf7e651f..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); } @@ -456,7 +470,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; } }); diff --git a/src/Config.js b/src/Config.js index 54e3cc5ca4..8af67cd543 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) { @@ -552,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/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/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/Controllers/FilesController.js b/src/Controllers/FilesController.js index a88c527b00..e21ea7c1a3 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}-.*' @@ -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/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 973e8b301b..7a3c5043df 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.

Default is `false`.
Requires option `verifyUserEmails: true`.', - action: parsers.booleanParser, + 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: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,8 @@ 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, }, serverCloseComplete: { @@ -582,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: { @@ -604,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: { @@ -622,15 +585,14 @@ 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.

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:Default is `false`.", + action: parsers.booleanOrFunctionParser, default: false, }, webhookKey: { @@ -641,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:Default is `ip`.', + help: 'The type of rate limit to apply. The following types are supported: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: { @@ -710,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, }, @@ -739,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', @@ -769,14 +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.", - default: './public', + 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: {}, }, @@ -903,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', @@ -950,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: { @@ -968,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, }, @@ -984,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, }, @@ -1005,60 +937,57 @@ 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 = { + 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.', @@ -1079,8 +1008,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]?$)'], }, @@ -1121,147 +1049,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, }, @@ -1272,20 +1177,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: { @@ -1296,60 +1198,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', @@ -1361,14 +1253,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', @@ -1382,31 +1272,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: { @@ -1415,8 +1300,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: { @@ -1426,25 +1310,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', @@ -1457,14 +1337,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'), }, }; @@ -1490,38 +1368,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', }, }; diff --git a/src/Options/docs.js b/src/Options/docs.js index fd553db46d..bbe4bf56ed 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 */ @@ -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. */ @@ -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 5278059ee0..263154f9d6 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: + 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. */ @@ -411,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 */ @@ -605,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/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/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/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; diff --git a/src/RestWrite.js b/src/RestWrite.js index a0de5577a5..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) !== @@ -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(); @@ -771,6 +779,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 +858,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 +994,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 +1019,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/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/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/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); 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/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; diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index 8cee0f6ea5..4b2e75fb2c 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; @@ -220,6 +236,7 @@ export interface PasswordPolicyOptions { resetPasswordSuccessOnInvalidEmail?: boolean; } export interface FileUploadOptions { + allowedFileUrlDomains?: string[]; fileExtensions?: (string[]); enableForAnonymousUser?: boolean; enableForAuthenticatedUser?: boolean;