diff --git a/.github/workflows/ci-performance.yml b/.github/workflows/ci-performance.yml
new file mode 100644
index 0000000000..c9cb055e13
--- /dev/null
+++ b/.github/workflows/ci-performance.yml
@@ -0,0 +1,328 @@
+name: ci-performance
+on:
+ pull_request_target:
+ branches:
+ - alpha
+ - beta
+ - release
+ - 'release-[0-9]+.x.x'
+ - next-major
+ paths-ignore:
+ - '**.md'
+ - 'docs/**'
+
+env:
+ NODE_VERSION: 24.11.0
+ MONGODB_VERSION: 8.0.4
+
+permissions:
+ contents: read
+ pull-requests: write
+ issues: write
+
+jobs:
+ performance-check:
+ name: Benchmarks
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout PR branch (for benchmark script)
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ fetch-depth: 1
+
+ - name: Save PR benchmark script
+ run: |
+ mkdir -p /tmp/pr-benchmark
+ cp -r benchmark /tmp/pr-benchmark/ || echo "No benchmark directory"
+ cp package.json /tmp/pr-benchmark/ || true
+
+ - name: Checkout base branch
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.base_ref }}
+ fetch-depth: 1
+ clean: true
+
+ - name: Restore PR benchmark script
+ run: |
+ if [ -d "/tmp/pr-benchmark/benchmark" ]; then
+ rm -rf benchmark
+ cp -r /tmp/pr-benchmark/benchmark .
+ fi
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: 'npm'
+
+ - name: Install dependencies (base)
+ run: npm ci
+
+ - name: Build Parse Server (base)
+ run: npm run build
+
+ - name: Run baseline benchmarks
+ id: baseline
+ env:
+ NODE_ENV: production
+ run: |
+ echo "Running baseline benchmarks..."
+ if [ ! -f "benchmark/performance.js" ]; then
+ echo "⚠️ Benchmark script not found - this is expected for new features"
+ echo "Skipping baseline benchmark"
+ echo '[]' > baseline.json
+ echo "Baseline: N/A (no benchmark script)" > baseline-output.txt
+ exit 0
+ fi
+ taskset -c 0 npm run benchmark > baseline-output.txt 2>&1 || npm run benchmark > baseline-output.txt 2>&1 || true
+ echo "Benchmark command completed with exit code: $?"
+ echo "Output file size: $(wc -c < baseline-output.txt) bytes"
+ echo "--- Begin baseline-output.txt ---"
+ cat baseline-output.txt
+ echo "--- End baseline-output.txt ---"
+ # Extract JSON from output (everything between first [ and last ])
+ sed -n '/^\[/,/^\]/p' baseline-output.txt > baseline.json || echo '[]' > baseline.json
+ echo "Extracted JSON size: $(wc -c < baseline.json) bytes"
+ echo "Baseline benchmark results:"
+ cat baseline.json
+ continue-on-error: true
+
+ - name: Save baseline results to temp location
+ run: |
+ mkdir -p /tmp/benchmark-results
+ cp baseline.json /tmp/benchmark-results/ || echo '[]' > /tmp/benchmark-results/baseline.json
+ cp baseline-output.txt /tmp/benchmark-results/ || echo 'No baseline output' > /tmp/benchmark-results/baseline-output.txt
+
+ - name: Upload baseline results
+ uses: actions/upload-artifact@v4
+ with:
+ name: baseline-benchmark
+ path: |
+ /tmp/benchmark-results/baseline.json
+ /tmp/benchmark-results/baseline-output.txt
+ retention-days: 7
+
+ - name: Checkout PR branch
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ fetch-depth: 1
+ clean: true
+
+ - name: Restore baseline results
+ run: |
+ cp /tmp/benchmark-results/baseline.json ./ || echo '[]' > baseline.json
+ cp /tmp/benchmark-results/baseline-output.txt ./ || echo 'No baseline output' > baseline-output.txt
+
+ - name: Setup Node.js (PR)
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: 'npm'
+
+ - name: Install dependencies (PR)
+ run: npm ci
+
+ - name: Build Parse Server (PR)
+ run: npm run build
+
+ - name: Run PR benchmarks
+ id: pr-bench
+ env:
+ NODE_ENV: production
+ run: |
+ echo "Running PR benchmarks..."
+ taskset -c 0 npm run benchmark > pr-output.txt 2>&1 || npm run benchmark > pr-output.txt 2>&1 || true
+ echo "Benchmark command completed with exit code: $?"
+ echo "Output file size: $(wc -c < pr-output.txt) bytes"
+ echo "--- Begin pr-output.txt ---"
+ cat pr-output.txt
+ echo "--- End pr-output.txt ---"
+ # Extract JSON from output (everything between first [ and last ])
+ sed -n '/^\[/,/^\]/p' pr-output.txt > pr.json || echo '[]' > pr.json
+ echo "Extracted JSON size: $(wc -c < pr.json) bytes"
+ echo "PR benchmark results:"
+ cat pr.json
+ continue-on-error: true
+
+ - name: Upload PR results
+ uses: actions/upload-artifact@v4
+ with:
+ name: pr-benchmark
+ path: |
+ pr.json
+ pr-output.txt
+ retention-days: 7
+
+ - name: Verify benchmark files exist
+ run: |
+ echo "Checking for benchmark result files..."
+ if [ ! -f baseline.json ] || [ ! -s baseline.json ]; then
+ echo "⚠️ baseline.json is missing or empty, creating empty array"
+ echo '[]' > baseline.json
+ fi
+ if [ ! -f pr.json ] || [ ! -s pr.json ]; then
+ echo "⚠️ pr.json is missing or empty, creating empty array"
+ echo '[]' > pr.json
+ fi
+ echo "baseline.json size: $(wc -c < baseline.json) bytes"
+ echo "pr.json size: $(wc -c < pr.json) bytes"
+
+ - name: Store benchmark result (PR)
+ uses: benchmark-action/github-action-benchmark@v1
+ if: github.event_name == 'pull_request' && hashFiles('pr.json') != ''
+ continue-on-error: true
+ with:
+ name: Parse Server Performance
+ tool: 'customSmallerIsBetter'
+ output-file-path: pr.json
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ auto-push: false
+ save-data-file: false
+ alert-threshold: '110%'
+ comment-on-alert: true
+ fail-on-alert: false
+ alert-comment-cc-users: '@parse-community/maintainers'
+ summary-always: true
+
+ - name: Compare benchmark results
+ id: compare
+ run: |
+ node -e "
+ const fs = require('fs');
+
+ let baseline, pr;
+ try {
+ baseline = JSON.parse(fs.readFileSync('baseline.json', 'utf8'));
+ pr = JSON.parse(fs.readFileSync('pr.json', 'utf8'));
+ } catch (e) {
+ console.log('⚠️ Could not parse benchmark results');
+ process.exit(0);
+ }
+
+ // Handle case where baseline doesn't exist (new feature)
+ if (!Array.isArray(baseline) || baseline.length === 0) {
+ if (!Array.isArray(pr) || pr.length === 0) {
+ console.log('⚠️ Benchmark results are empty or invalid');
+ process.exit(0);
+ }
+ console.log('# Performance Benchmark Results\n');
+ console.log('> ℹ️ Baseline not available - this appears to be a new feature\n');
+ console.log('| Benchmark | Value | Details |');
+ console.log('|-----------|-------|---------|');
+ pr.forEach(result => {
+ console.log(\`| \${result.name} | \${result.value.toFixed(2)} ms | \${result.extra} |\`);
+ });
+ console.log('');
+ console.log('✅ **New benchmarks established for this feature.**');
+ process.exit(0);
+ }
+
+ if (!Array.isArray(pr) || pr.length === 0) {
+ console.log('⚠️ PR benchmark results are empty or invalid');
+ process.exit(0);
+ }
+
+ console.log('# Performance Comparison\n');
+ console.log('| Benchmark | Baseline | PR | Change | Status |');
+ console.log('|-----------|----------|----|---------| ------ |');
+
+ let hasRegression = false;
+ let hasImprovement = false;
+
+ baseline.forEach(baseResult => {
+ const prResult = pr.find(p => p.name === baseResult.name);
+ if (!prResult) {
+ console.log(\`| \${baseResult.name} | \${baseResult.value.toFixed(2)} ms | N/A | - | ⚠️ Missing |\`);
+ return;
+ }
+
+ const baseValue = parseFloat(baseResult.value);
+ const prValue = parseFloat(prResult.value);
+ const change = ((prValue - baseValue) / baseValue * 100);
+ const changeStr = change > 0 ? \`+\${change.toFixed(1)}%\` : \`\${change.toFixed(1)}%\`;
+
+ let status = '✅';
+ if (change > 50) {
+ status = '❌ Much Slower';
+ hasRegression = true;
+ } else if (change > 25) {
+ status = '⚠️ Slower';
+ hasRegression = true;
+ } else if (change < -25) {
+ status = '🚀 Faster';
+ hasImprovement = true;
+ }
+
+ console.log(\`| \${baseResult.name} | \${baseValue.toFixed(2)} ms | \${prValue.toFixed(2)} ms | \${changeStr} | \${status} |\`);
+ });
+
+ console.log('');
+ if (hasRegression) {
+ console.log('⚠️ **Performance regressions detected.** Please review the changes.');
+ } else if (hasImprovement) {
+ console.log('🚀 **Performance improvements detected!** Great work!');
+ } else {
+ console.log('✅ **No significant performance changes.**');
+ }
+ " | tee comparison.md
+
+ - name: Upload comparison
+ uses: actions/upload-artifact@v4
+ with:
+ name: benchmark-comparison
+ path: comparison.md
+ retention-days: 30
+
+ - name: Prepare comment body
+ if: github.event_name == 'pull_request'
+ run: |
+ echo "## Performance Impact Report" > comment.md
+ echo "" >> comment.md
+ if [ -f comparison.md ]; then
+ cat comparison.md >> comment.md
+ else
+ echo "⚠️ Could not generate performance comparison." >> comment.md
+ fi
+ echo "" >> comment.md
+ echo "" >> comment.md
+ echo "📊 View detailed results
" >> comment.md
+ echo "" >> comment.md
+ echo "### Baseline Results" >> comment.md
+ echo "\`\`\`json" >> comment.md
+ cat baseline.json >> comment.md
+ echo "\`\`\`" >> comment.md
+ echo "" >> comment.md
+ echo "### PR Results" >> comment.md
+ echo "\`\`\`json" >> comment.md
+ cat pr.json >> comment.md
+ echo "\`\`\`" >> comment.md
+ echo "" >> comment.md
+ echo " " >> comment.md
+ echo "" >> comment.md
+ echo "> **Note:** Thresholds: ⚠️ >25%, ❌ >50%." >> comment.md
+
+ - name: Comment PR with results
+ if: github.event_name == 'pull_request'
+ uses: thollander/actions-comment-pull-request@v2
+ continue-on-error: true
+ with:
+ filePath: comment.md
+ comment_tag: performance-benchmark
+ mode: recreate
+
+ - name: Generate job summary
+ if: always()
+ run: |
+ if [ -f comparison.md ]; then
+ cat comparison.md >> $GITHUB_STEP_SUMMARY
+ else
+ echo "⚠️ Benchmark comparison not available" >> $GITHUB_STEP_SUMMARY
+ fi
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 01c88df10c..30050f87a6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,9 +21,13 @@
- [Good to Know](#good-to-know)
- [Troubleshooting](#troubleshooting)
- [Please Do's](#please-dos)
- - [TypeScript Tests](#typescript-tests)
+ - [TypeScript Tests](#typescript-tests)
- [Test against Postgres](#test-against-postgres)
- [Postgres with Docker](#postgres-with-docker)
+ - [Performance Testing](#performance-testing)
+ - [Adding Tests](#adding-tests)
+ - [Adding Benchmarks](#adding-benchmarks)
+ - [Benchmark Guidelines](#benchmark-guidelines)
- [Breaking Changes](#breaking-changes)
- [Deprecation Policy](#deprecation-policy)
- [Feature Considerations](#feature-considerations)
@@ -298,6 +302,58 @@ RUN chmod +x /docker-entrypoint-initdb.d/setup-dbs.sh
Note that the script above will ONLY be executed during initialization of the container with no data in the database, see the official [Postgres image](https://hub.docker.com/_/postgres) for details. If you want to use the script to run again be sure there is no data in the /var/lib/postgresql/data of the container.
+### Performance Testing
+
+Parse Server includes an automated performance benchmarking system that runs on every pull request to detect performance regressions and track improvements over time.
+
+#### Adding Tests
+
+You should consider adding performance benchmarks if your contribution:
+
+- **Introduces a performance-critical feature**: Features that will be frequently used in production environments, such as new query operations, authentication methods, or data processing functions.
+- **Modifies existing critical paths**: Changes to core functionality like object CRUD operations, query execution, user authentication, file operations, or Cloud Code execution.
+- **Has potential performance impact**: Any change that affects database operations, network requests, data parsing, caching mechanisms, or algorithmic complexity.
+- **Optimizes performance**: If your PR specifically aims to improve performance, adding benchmarks helps verify the improvement and prevents future regressions.
+
+#### Adding Benchmarks
+
+Performance benchmarks are located in [`benchmark/performance.js`](benchmark/performance.js). To add a new benchmark:
+
+1. **Identify the operation to benchmark**: Determine the specific operation you want to measure (e.g., a new query type, a new API endpoint).
+
+2. **Create a benchmark function**: Follow the existing patterns in `benchmark/performance.js`:
+ ```javascript
+ async function benchmarkNewFeature() {
+ return measureOperation('Feature Name', async () => {
+ // Your operation to benchmark
+ const result = await someOperation();
+ }, ITERATIONS);
+ }
+ ```
+
+3. **Add to benchmark suite**: Register your benchmark in the `runBenchmarks()` function:
+ ```javascript
+ console.error('Running New Feature benchmark...');
+ await cleanupDatabase();
+ results.push(await benchmarkNewFeature());
+ ```
+
+4. **Test locally**: Run the benchmarks locally to verify they work:
+ ```bash
+ npm run benchmark:quick # Quick test with 10 iterations
+ npm run benchmark # Full test with 10,000 iterations
+ ```
+
+For new features where no baseline exists, the CI will establish new benchmarks that future PRs will be compared against.
+
+#### Benchmark Guidelines
+
+- **Keep benchmarks focused**: Each benchmark should test a single, well-defined operation.
+- **Use realistic data**: Test with data that reflects real-world usage patterns.
+- **Clean up between runs**: Use `cleanupDatabase()` to ensure consistent test conditions.
+- **Consider iteration count**: Use fewer iterations for expensive operations (see `ITERATIONS` environment variable).
+- **Document what you're testing**: Add clear comments explaining what the benchmark measures and why it's important.
+
## Breaking Changes
Breaking changes should be avoided whenever possible. For a breaking change to be accepted, the benefits of the change have to clearly outweigh the costs of developers having to adapt their deployments. If a breaking change is only cosmetic it will likely be rejected and preferred to become obsolete organically during the course of further development, unless it is required as part of a larger change. Breaking changes should follow the [Deprecation Policy](#deprecation-policy).
diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md
index eb7f463638..6ac20b4616 100644
--- a/DEPRECATIONS.md
+++ b/DEPRECATIONS.md
@@ -2,19 +2,20 @@
The following is a list of deprecations, according to the [Deprecation Policy](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md#deprecation-policy). After a feature becomes deprecated, and giving developers time to adapt to the change, the deprecated feature will eventually be removed, leading to a breaking change. Developer feedback during the deprecation period may postpone or even revoke the introduction of the breaking change.
-| ID | Change | Issue | Deprecation [ℹ️][i_deprecation] | Planned Removal [ℹ️][i_removal] | Status [ℹ️][i_status] | Notes |
-|--------|-------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------|
-| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
-| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
-| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
-| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
-| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
-| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
-| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
-| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
-| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | removed | - |
-| DEPPS10 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | deprecated | - |
-| DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | deprecated | - |
+| ID | Change | Issue | Deprecation [ℹ️][i_deprecation] | Planned Removal [ℹ️][i_removal] | Status [ℹ️][i_status] | Notes |
+|---------|----------------------------------------------------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------|
+| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
+| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
+| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
+| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
+| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
+| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
+| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
+| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
+| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | removed | - |
+| DEPPS10 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | deprecated | - |
+| DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | deprecated | - |
+| DEPPS12 | Database option `allowPublicExplain` will default to `true` | [#7519](https://github.com/parse-community/parse-server/issues/7519) | 8.5.0 (2025) | 9.0.0 (2026) | deprecated | - |
[i_deprecation]: ## "The version and date of the deprecation."
[i_removal]: ## "The version and date of the planned removal."
diff --git a/benchmark/MongoLatencyWrapper.js b/benchmark/MongoLatencyWrapper.js
new file mode 100644
index 0000000000..2b0480c1bc
--- /dev/null
+++ b/benchmark/MongoLatencyWrapper.js
@@ -0,0 +1,137 @@
+/**
+ * MongoDB Latency Wrapper
+ *
+ * Utility to inject artificial latency into MongoDB operations for performance testing.
+ * This wrapper temporarily wraps MongoDB Collection methods to add delays before
+ * database operations execute.
+ *
+ * Usage:
+ * const { wrapMongoDBWithLatency } = require('./MongoLatencyWrapper');
+ *
+ * // Before initializing Parse Server
+ * const unwrap = wrapMongoDBWithLatency(10); // 10ms delay
+ *
+ * // ... run benchmarks ...
+ *
+ * // Cleanup when done
+ * unwrap();
+ */
+
+const { Collection } = require('mongodb');
+
+// Store original methods for restoration
+const originalMethods = new Map();
+
+/**
+ * Wrap a Collection method to add artificial latency
+ * @param {string} methodName - Name of the method to wrap
+ * @param {number} latencyMs - Delay in milliseconds
+ */
+function wrapMethod(methodName, latencyMs) {
+ if (!originalMethods.has(methodName)) {
+ originalMethods.set(methodName, Collection.prototype[methodName]);
+ }
+
+ const originalMethod = originalMethods.get(methodName);
+
+ Collection.prototype[methodName] = function (...args) {
+ // For methods that return cursors (like find, aggregate), we need to delay the execution
+ // but still return a cursor-like object
+ const result = originalMethod.apply(this, args);
+
+ // Check if result has cursor methods (toArray, forEach, etc.)
+ if (result && typeof result.toArray === 'function') {
+ // Wrap cursor methods that actually execute the query
+ const originalToArray = result.toArray.bind(result);
+ result.toArray = function() {
+ // Wait for the original promise to settle, then delay the result
+ return originalToArray().then(
+ value => new Promise(resolve => setTimeout(() => resolve(value), latencyMs)),
+ error => new Promise((_, reject) => setTimeout(() => reject(error), latencyMs))
+ );
+ };
+ return result;
+ }
+
+ // For promise-returning methods, wrap the promise with delay
+ if (result && typeof result.then === 'function') {
+ // Wait for the original promise to settle, then delay the result
+ return result.then(
+ value => new Promise(resolve => setTimeout(() => resolve(value), latencyMs)),
+ error => new Promise((_, reject) => setTimeout(() => reject(error), latencyMs))
+ );
+ }
+
+ // For synchronous methods, just add delay
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(result);
+ }, latencyMs);
+ });
+ };
+}
+
+/**
+ * Wrap MongoDB Collection methods with artificial latency
+ * @param {number} latencyMs - Delay in milliseconds to inject before each operation
+ * @returns {Function} unwrap - Function to restore original methods
+ */
+function wrapMongoDBWithLatency(latencyMs) {
+ if (typeof latencyMs !== 'number' || latencyMs < 0) {
+ throw new Error('latencyMs must be a non-negative number');
+ }
+
+ if (latencyMs === 0) {
+ // eslint-disable-next-line no-console
+ console.log('Latency is 0ms, skipping MongoDB wrapping');
+ return () => {}; // No-op unwrap function
+ }
+
+ // eslint-disable-next-line no-console
+ console.log(`Wrapping MongoDB operations with ${latencyMs}ms artificial latency`);
+
+ // List of MongoDB Collection methods to wrap
+ const methodsToWrap = [
+ 'find',
+ 'findOne',
+ 'countDocuments',
+ 'estimatedDocumentCount',
+ 'distinct',
+ 'aggregate',
+ 'insertOne',
+ 'insertMany',
+ 'updateOne',
+ 'updateMany',
+ 'replaceOne',
+ 'deleteOne',
+ 'deleteMany',
+ 'findOneAndUpdate',
+ 'findOneAndReplace',
+ 'findOneAndDelete',
+ 'createIndex',
+ 'createIndexes',
+ 'dropIndex',
+ 'dropIndexes',
+ 'drop',
+ ];
+
+ methodsToWrap.forEach(methodName => {
+ wrapMethod(methodName, latencyMs);
+ });
+
+ // Return unwrap function to restore original methods
+ return function unwrap() {
+ // eslint-disable-next-line no-console
+ console.log('Removing MongoDB latency wrapper, restoring original methods');
+
+ originalMethods.forEach((originalMethod, methodName) => {
+ Collection.prototype[methodName] = originalMethod;
+ });
+
+ originalMethods.clear();
+ };
+}
+
+module.exports = {
+ wrapMongoDBWithLatency,
+};
diff --git a/benchmark/performance.js b/benchmark/performance.js
new file mode 100644
index 0000000000..c37ac09777
--- /dev/null
+++ b/benchmark/performance.js
@@ -0,0 +1,598 @@
+/**
+ * Performance Benchmark Suite for Parse Server
+ *
+ * This suite measures the performance of critical Parse Server operations
+ * using the Node.js Performance API. Results are output in a format
+ * compatible with github-action-benchmark.
+ *
+ * Run with: npm run benchmark
+ */
+
+const core = require('@actions/core');
+const Parse = require('parse/node');
+const { performance } = require('node:perf_hooks');
+const { MongoClient } = require('mongodb');
+const { wrapMongoDBWithLatency } = require('./MongoLatencyWrapper');
+
+// Configuration
+const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/parse_benchmark_test';
+const SERVER_URL = 'http://localhost:1337/parse';
+const APP_ID = 'benchmark-app-id';
+const MASTER_KEY = 'benchmark-master-key';
+const ITERATIONS = process.env.BENCHMARK_ITERATIONS ? parseInt(process.env.BENCHMARK_ITERATIONS, 10) : undefined;
+const LOG_ITERATIONS = false;
+
+// Parse Server instance
+let parseServer;
+let mongoClient;
+
+// Logging helpers
+const logInfo = message => core.info(message);
+const logError = message => core.error(message);
+
+/**
+ * Initialize Parse Server for benchmarking
+ */
+async function initializeParseServer() {
+ const express = require('express');
+ const { default: ParseServer } = require('../lib/index.js');
+
+ const app = express();
+
+ parseServer = new ParseServer({
+ databaseURI: MONGODB_URI,
+ appId: APP_ID,
+ masterKey: MASTER_KEY,
+ serverURL: SERVER_URL,
+ silent: true,
+ allowClientClassCreation: true,
+ logLevel: 'error', // Minimal logging for performance
+ verbose: false,
+ });
+
+ app.use('/parse', parseServer.app);
+
+ return new Promise((resolve, reject) => {
+ const server = app.listen(1337, (err) => {
+ if (err) {
+ reject(new Error(`Failed to start server: ${err.message}`));
+ return;
+ }
+ Parse.initialize(APP_ID);
+ Parse.masterKey = MASTER_KEY;
+ Parse.serverURL = SERVER_URL;
+ resolve(server);
+ });
+
+ server.on('error', (err) => {
+ reject(new Error(`Server error: ${err.message}`));
+ });
+ });
+}
+
+/**
+ * Clean up database between benchmarks
+ */
+async function cleanupDatabase() {
+ try {
+ if (!mongoClient) {
+ mongoClient = await MongoClient.connect(MONGODB_URI);
+ }
+ const db = mongoClient.db();
+ const collections = await db.listCollections().toArray();
+
+ for (const collection of collections) {
+ if (!collection.name.startsWith('system.')) {
+ await db.collection(collection.name).deleteMany({});
+ }
+ }
+ } catch (error) {
+ throw new Error(`Failed to cleanup database: ${error.message}`);
+ }
+}
+
+/**
+ * Reset Parse SDK to use the default server
+ */
+function resetParseServer() {
+ Parse.serverURL = SERVER_URL;
+}
+
+/**
+ * Measure average time for an async operation over multiple iterations.
+ * @param {Object} options Measurement options.
+ * @param {string} options.name Name of the operation being measured.
+ * @param {Function} options.operation Async function to measure.
+ * @param {number} options.iterations Number of iterations to run; choose a value that is high
+ * enough to create reliable benchmark metrics with low variance but low enough to keep test
+ * duration reasonable around <=10 seconds.
+ * @param {boolean} [options.skipWarmup=false] Skip warmup phase.
+ * @param {number} [options.dbLatency] Artificial DB latency in milliseconds to apply during
+ * this benchmark.
+ */
+async function measureOperation({ name, operation, iterations, skipWarmup = false, dbLatency }) {
+ // Override iterations if global ITERATIONS is set
+ iterations = ITERATIONS || iterations;
+
+ // Determine warmup count (20% of iterations)
+ const warmupCount = skipWarmup ? 0 : Math.floor(iterations * 0.2);
+ const times = [];
+
+ // Apply artificial latency if specified
+ let unwrapLatency = null;
+ if (dbLatency !== undefined && dbLatency > 0) {
+ logInfo(`Applying ${dbLatency}ms artificial DB latency for this benchmark`);
+ unwrapLatency = wrapMongoDBWithLatency(dbLatency);
+ }
+
+ try {
+ if (warmupCount > 0) {
+ logInfo(`Starting warmup phase of ${warmupCount} iterations...`);
+ const warmupStart = performance.now();
+ for (let i = 0; i < warmupCount; i++) {
+ await operation();
+ }
+ logInfo(`Warmup took: ${(performance.now() - warmupStart).toFixed(2)}ms`);
+ }
+
+ // Measurement phase
+ logInfo(`Starting measurement phase of ${iterations} iterations...`);
+ const progressInterval = Math.ceil(iterations / 10); // Log every 10%
+ const measurementStart = performance.now();
+
+ for (let i = 0; i < iterations; i++) {
+ const start = performance.now();
+ await operation();
+ const end = performance.now();
+ const duration = end - start;
+ times.push(duration);
+
+ // Log progress every 10% or individual iterations if LOG_ITERATIONS is enabled
+ if (LOG_ITERATIONS) {
+ logInfo(`Iteration ${i + 1}: ${duration.toFixed(2)}ms`);
+ } else if ((i + 1) % progressInterval === 0 || i + 1 === iterations) {
+ const progress = Math.round(((i + 1) / iterations) * 100);
+ logInfo(`Progress: ${progress}%`);
+ }
+ }
+
+ logInfo(`Measurement took: ${(performance.now() - measurementStart).toFixed(2)}ms`);
+
+ // Sort times for percentile calculations
+ times.sort((a, b) => a - b);
+
+ // Filter outliers using Interquartile Range (IQR) method
+ const q1Index = Math.floor(times.length * 0.25);
+ const q3Index = Math.floor(times.length * 0.75);
+ const q1 = times[q1Index];
+ const q3 = times[q3Index];
+ const iqr = q3 - q1;
+ const lowerBound = q1 - 1.5 * iqr;
+ const upperBound = q3 + 1.5 * iqr;
+
+ const filtered = times.filter(t => t >= lowerBound && t <= upperBound);
+
+ // Calculate statistics on filtered data
+ const median = filtered[Math.floor(filtered.length * 0.5)];
+ const p95 = filtered[Math.floor(filtered.length * 0.95)];
+ const p99 = filtered[Math.floor(filtered.length * 0.99)];
+ const min = filtered[0];
+ const max = filtered[filtered.length - 1];
+
+ return {
+ name,
+ value: median, // Use median (p50) as primary metric for stability in CI
+ unit: 'ms',
+ range: `${min.toFixed(2)} - ${max.toFixed(2)}`,
+ extra: `p95: ${p95.toFixed(2)}ms, p99: ${p99.toFixed(2)}ms, n=${filtered.length}/${times.length}`,
+ };
+ } finally {
+ // Remove latency wrapper if it was applied
+ if (unwrapLatency) {
+ unwrapLatency();
+ logInfo('Removed artificial DB latency');
+ }
+ }
+}
+
+/**
+ * Benchmark: Object Create
+ */
+async function benchmarkObjectCreate(name) {
+ let counter = 0;
+
+ return measureOperation({
+ name,
+ iterations: 1_000,
+ operation: async () => {
+ const TestObject = Parse.Object.extend('BenchmarkTest');
+ const obj = new TestObject();
+ obj.set('testField', `test-value-${counter++}`);
+ obj.set('number', counter);
+ obj.set('boolean', true);
+ await obj.save();
+ },
+ });
+}
+
+/**
+ * Benchmark: Object Read (by ID)
+ */
+async function benchmarkObjectRead(name) {
+ // Setup: Create test objects
+ const TestObject = Parse.Object.extend('BenchmarkTest');
+ const objects = [];
+
+ for (let i = 0; i < 1_000; i++) {
+ const obj = new TestObject();
+ obj.set('testField', `read-test-${i}`);
+ objects.push(obj);
+ }
+
+ await Parse.Object.saveAll(objects);
+
+ let counter = 0;
+
+ return measureOperation({
+ name,
+ iterations: 1_000,
+ operation: async () => {
+ const query = new Parse.Query('BenchmarkTest');
+ await query.get(objects[counter++ % objects.length].id);
+ },
+ });
+}
+
+/**
+ * Benchmark: Object Update
+ */
+async function benchmarkObjectUpdate(name) {
+ // Setup: Create test objects
+ const TestObject = Parse.Object.extend('BenchmarkTest');
+ const objects = [];
+
+ for (let i = 0; i < 1_000; i++) {
+ const obj = new TestObject();
+ obj.set('testField', `update-test-${i}`);
+ obj.set('counter', 0);
+ objects.push(obj);
+ }
+
+ await Parse.Object.saveAll(objects);
+
+ let counter = 0;
+
+ return measureOperation({
+ name,
+ iterations: 1_000,
+ operation: async () => {
+ const obj = objects[counter++ % objects.length];
+ obj.increment('counter');
+ obj.set('lastUpdated', new Date());
+ await obj.save();
+ },
+ });
+}
+
+/**
+ * Benchmark: Simple Query
+ */
+async function benchmarkSimpleQuery(name) {
+ // Setup: Create test data
+ const TestObject = Parse.Object.extend('BenchmarkTest');
+ const objects = [];
+
+ for (let i = 0; i < 100; i++) {
+ const obj = new TestObject();
+ obj.set('category', i % 10);
+ obj.set('value', i);
+ objects.push(obj);
+ }
+
+ await Parse.Object.saveAll(objects);
+
+ let counter = 0;
+
+ return measureOperation({
+ name,
+ iterations: 1_000,
+ operation: async () => {
+ const query = new Parse.Query('BenchmarkTest');
+ query.equalTo('category', counter++ % 10);
+ await query.find();
+ },
+ });
+}
+
+/**
+ * Benchmark: Batch Save (saveAll)
+ */
+async function benchmarkBatchSave(name) {
+ const BATCH_SIZE = 10;
+
+ return measureOperation({
+ name,
+ iterations: 1_000,
+ operation: async () => {
+ const TestObject = Parse.Object.extend('BenchmarkTest');
+ const objects = [];
+
+ for (let i = 0; i < BATCH_SIZE; i++) {
+ const obj = new TestObject();
+ obj.set('batchField', `batch-${i}`);
+ obj.set('timestamp', new Date());
+ objects.push(obj);
+ }
+
+ await Parse.Object.saveAll(objects);
+ },
+ });
+}
+
+/**
+ * Benchmark: User Signup
+ */
+async function benchmarkUserSignup(name) {
+ let counter = 0;
+
+ return measureOperation({
+ name,
+ iterations: 500,
+ operation: async () => {
+ counter++;
+ const user = new Parse.User();
+ user.set('username', `benchmark_user_${Date.now()}_${counter}`);
+ user.set('password', 'benchmark_password');
+ user.set('email', `benchmark${counter}@example.com`);
+ await user.signUp();
+ },
+ });
+}
+
+/**
+ * Benchmark: User Login
+ */
+async function benchmarkUserLogin(name) {
+ // Setup: Create test users
+ const users = [];
+
+ for (let i = 0; i < 10; i++) {
+ const user = new Parse.User();
+ user.set('username', `benchmark_login_user_${i}`);
+ user.set('password', 'benchmark_password');
+ user.set('email', `login${i}@example.com`);
+ await user.signUp();
+ users.push({ username: user.get('username'), password: 'benchmark_password' });
+ await Parse.User.logOut();
+ }
+
+ let counter = 0;
+
+ return measureOperation({
+ name,
+ iterations: 500,
+ operation: async () => {
+ const userCreds = users[counter++ % users.length];
+ await Parse.User.logIn(userCreds.username, userCreds.password);
+ await Parse.User.logOut();
+ },
+ });
+}
+
+/**
+ * Benchmark: Query with Include (Parallel Pointers)
+ * Tests the performance improvement when fetching multiple pointers at the same level.
+ */
+async function benchmarkQueryWithIncludeParallel(name) {
+ const PointerAClass = Parse.Object.extend('PointerA');
+ const PointerBClass = Parse.Object.extend('PointerB');
+ const PointerCClass = Parse.Object.extend('PointerC');
+ const RootClass = Parse.Object.extend('Root');
+
+ // Create pointer objects
+ const pointerAObjects = [];
+ for (let i = 0; i < 10; i++) {
+ const obj = new PointerAClass();
+ obj.set('name', `pointerA-${i}`);
+ pointerAObjects.push(obj);
+ }
+ await Parse.Object.saveAll(pointerAObjects);
+
+ const pointerBObjects = [];
+ for (let i = 0; i < 10; i++) {
+ const obj = new PointerBClass();
+ obj.set('name', `pointerB-${i}`);
+ pointerBObjects.push(obj);
+ }
+ await Parse.Object.saveAll(pointerBObjects);
+
+ const pointerCObjects = [];
+ for (let i = 0; i < 10; i++) {
+ const obj = new PointerCClass();
+ obj.set('name', `pointerC-${i}`);
+ pointerCObjects.push(obj);
+ }
+ await Parse.Object.saveAll(pointerCObjects);
+
+ // Create Root objects with multiple pointers at the same level
+ const rootObjects = [];
+ for (let i = 0; i < 10; i++) {
+ const obj = new RootClass();
+ obj.set('name', `root-${i}`);
+ obj.set('pointerA', pointerAObjects[i % pointerAObjects.length]);
+ obj.set('pointerB', pointerBObjects[i % pointerBObjects.length]);
+ obj.set('pointerC', pointerCObjects[i % pointerCObjects.length]);
+ rootObjects.push(obj);
+ }
+ await Parse.Object.saveAll(rootObjects);
+
+ return measureOperation({
+ name,
+ skipWarmup: true,
+ dbLatency: 100,
+ iterations: 100,
+ operation: async () => {
+ const query = new Parse.Query('Root');
+ // Include multiple pointers at the same level - should fetch in parallel
+ query.include(['pointerA', 'pointerB', 'pointerC']);
+ await query.find();
+ },
+ });
+}
+
+/**
+ * Benchmark: Query with Include (Nested Pointers with Parallel Leaf Nodes)
+ * Tests the PR's optimization for parallel fetching at each nested level.
+ * Pattern: p1.p2.p3, p1.p2.p4, p1.p2.p5
+ * After fetching p2, we know the objectIds and can fetch p3, p4, p5 in parallel.
+ */
+async function benchmarkQueryWithIncludeNested(name) {
+ const Level3AClass = Parse.Object.extend('Level3A');
+ const Level3BClass = Parse.Object.extend('Level3B');
+ const Level3CClass = Parse.Object.extend('Level3C');
+ const Level2Class = Parse.Object.extend('Level2');
+ const Level1Class = Parse.Object.extend('Level1');
+ const RootClass = Parse.Object.extend('Root');
+
+ // Create Level3 objects (leaf nodes)
+ const level3AObjects = [];
+ for (let i = 0; i < 10; i++) {
+ const obj = new Level3AClass();
+ obj.set('name', `level3A-${i}`);
+ level3AObjects.push(obj);
+ }
+ await Parse.Object.saveAll(level3AObjects);
+
+ const level3BObjects = [];
+ for (let i = 0; i < 10; i++) {
+ const obj = new Level3BClass();
+ obj.set('name', `level3B-${i}`);
+ level3BObjects.push(obj);
+ }
+ await Parse.Object.saveAll(level3BObjects);
+
+ const level3CObjects = [];
+ for (let i = 0; i < 10; i++) {
+ const obj = new Level3CClass();
+ obj.set('name', `level3C-${i}`);
+ level3CObjects.push(obj);
+ }
+ await Parse.Object.saveAll(level3CObjects);
+
+ // Create Level2 objects pointing to multiple Level3 objects
+ const level2Objects = [];
+ for (let i = 0; i < 10; i++) {
+ const obj = new Level2Class();
+ obj.set('name', `level2-${i}`);
+ obj.set('level3A', level3AObjects[i % level3AObjects.length]);
+ obj.set('level3B', level3BObjects[i % level3BObjects.length]);
+ obj.set('level3C', level3CObjects[i % level3CObjects.length]);
+ level2Objects.push(obj);
+ }
+ await Parse.Object.saveAll(level2Objects);
+
+ // Create Level1 objects pointing to Level2
+ const level1Objects = [];
+ for (let i = 0; i < 10; i++) {
+ const obj = new Level1Class();
+ obj.set('name', `level1-${i}`);
+ obj.set('level2', level2Objects[i % level2Objects.length]);
+ level1Objects.push(obj);
+ }
+ await Parse.Object.saveAll(level1Objects);
+
+ // Create Root objects pointing to Level1
+ const rootObjects = [];
+ for (let i = 0; i < 10; i++) {
+ const obj = new RootClass();
+ obj.set('name', `root-${i}`);
+ obj.set('level1', level1Objects[i % level1Objects.length]);
+ rootObjects.push(obj);
+ }
+ await Parse.Object.saveAll(rootObjects);
+
+ return measureOperation({
+ name,
+ skipWarmup: true,
+ dbLatency: 100,
+ iterations: 100,
+ operation: async () => {
+ const query = new Parse.Query('Root');
+ // After fetching level1.level2, the PR should fetch level3A, level3B, level3C in parallel
+ query.include(['level1.level2.level3A', 'level1.level2.level3B', 'level1.level2.level3C']);
+ await query.find();
+ },
+ });
+}
+
+/**
+ * Run all benchmarks
+ */
+async function runBenchmarks() {
+ logInfo('Starting Parse Server Performance Benchmarks...');
+
+ let server;
+
+ try {
+ // Initialize Parse Server
+ logInfo('Initializing Parse Server...');
+ server = await initializeParseServer();
+
+ // Wait for server to be ready
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ const results = [];
+
+ // Define all benchmarks to run
+ const benchmarks = [
+ { name: 'Object.save (create)', fn: benchmarkObjectCreate },
+ { name: 'Object.save (update)', fn: benchmarkObjectUpdate },
+ { name: 'Object.saveAll (batch save)', fn: benchmarkBatchSave },
+ { name: 'Query.get (by objectId)', fn: benchmarkObjectRead },
+ { name: 'Query.find (simple query)', fn: benchmarkSimpleQuery },
+ { name: 'User.signUp', fn: benchmarkUserSignup },
+ { name: 'User.login', fn: benchmarkUserLogin },
+ { name: 'Query.include (parallel pointers)', fn: benchmarkQueryWithIncludeParallel },
+ { name: 'Query.include (nested pointers)', fn: benchmarkQueryWithIncludeNested },
+ ];
+
+ // Run each benchmark with database cleanup
+ for (const benchmark of benchmarks) {
+ logInfo(`\nRunning benchmark '${benchmark.name}'...`);
+ resetParseServer();
+ await cleanupDatabase();
+ results.push(await benchmark.fn(benchmark.name));
+ }
+
+ // Output results in github-action-benchmark format (stdout)
+ logInfo(JSON.stringify(results, null, 2));
+
+ // Output summary to stderr for visibility
+ logInfo('Benchmarks completed successfully!');
+ logInfo('Summary:');
+ results.forEach(result => {
+ logInfo(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`);
+ });
+
+ } catch (error) {
+ logError('Error running benchmarks:', error);
+ process.exit(1);
+ } finally {
+ // Cleanup
+ if (mongoClient) {
+ await mongoClient.close();
+ }
+ if (server) {
+ server.close();
+ }
+ // Give some time for cleanup
+ setTimeout(() => process.exit(0), 1000);
+ }
+}
+
+// Run benchmarks if executed directly
+if (require.main === module) {
+ runBenchmarks();
+}
+
+module.exports = { runBenchmarks };
diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md
index bf414dc3f8..854b825c1f 100644
--- a/changelogs/CHANGELOG_alpha.md
+++ b/changelogs/CHANGELOG_alpha.md
@@ -1,3 +1,129 @@
+# [8.5.0-alpha.18](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.17...8.5.0-alpha.18) (2025-12-01)
+
+
+### Features
+
+* Upgrade to parse 7.1.2 ([#9955](https://github.com/parse-community/parse-server/issues/9955)) ([5c644a5](https://github.com/parse-community/parse-server/commit/5c644a55ac25986f214b68ba4bcbe7a62ad6d6d1))
+
+# [8.5.0-alpha.17](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.16...8.5.0-alpha.17) (2025-12-01)
+
+
+### Features
+
+* Upgrade to parse 7.1.1 ([#9954](https://github.com/parse-community/parse-server/issues/9954)) ([fa57d69](https://github.com/parse-community/parse-server/commit/fa57d69cbec525189da98d7136c1c0e9eaf74338))
+
+# [8.5.0-alpha.16](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.15...8.5.0-alpha.16) (2025-11-28)
+
+
+### Features
+
+* Add Parse Server option `enableSanitizedErrorResponse` to remove detailed error messages from responses sent to clients ([#9944](https://github.com/parse-community/parse-server/issues/9944)) ([4752197](https://github.com/parse-community/parse-server/commit/47521974aeafcf41102be62f19612a4ab0a4837f))
+
+# [8.5.0-alpha.15](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.14...8.5.0-alpha.15) (2025-11-23)
+
+
+### Performance Improvements
+
+* Remove unused dependencies ([#9943](https://github.com/parse-community/parse-server/issues/9943)) ([d4c6de0](https://github.com/parse-community/parse-server/commit/d4c6de0096b3ac95289c6bddfe25eb397d790e41))
+
+# [8.5.0-alpha.14](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.13...8.5.0-alpha.14) (2025-11-23)
+
+
+### Bug Fixes
+
+* Parse Server option `rateLimit.zone` does not use default value `ip` ([#9941](https://github.com/parse-community/parse-server/issues/9941)) ([12beb8f](https://github.com/parse-community/parse-server/commit/12beb8f6ee5d3002fec017bb4525eb3f1375f806))
+
+# [8.5.0-alpha.13](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.12...8.5.0-alpha.13) (2025-11-23)
+
+
+### Bug Fixes
+
+* Server internal error details leaking in error messages returned to clients ([#9937](https://github.com/parse-community/parse-server/issues/9937)) ([50edb5a](https://github.com/parse-community/parse-server/commit/50edb5ab4bb4a6ce474bfb7cf159d918933753b8))
+
+# [8.5.0-alpha.12](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.11...8.5.0-alpha.12) (2025-11-19)
+
+
+### Features
+
+* Add `beforePasswordResetRequest` hook ([#9906](https://github.com/parse-community/parse-server/issues/9906)) ([94cee5b](https://github.com/parse-community/parse-server/commit/94cee5bfafca10c914c73cf17fcdb627a9f0837b))
+
+# [8.5.0-alpha.11](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.10...8.5.0-alpha.11) (2025-11-17)
+
+
+### Bug Fixes
+
+* Deprecation warning logged at server launch for nested Parse Server option even if option is explicitly set ([#9934](https://github.com/parse-community/parse-server/issues/9934)) ([c22cb0a](https://github.com/parse-community/parse-server/commit/c22cb0ae58e64cd0e4597ab9610d57a1155c44a2))
+
+# [8.5.0-alpha.10](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.9...8.5.0-alpha.10) (2025-11-17)
+
+
+### Bug Fixes
+
+* Queries with object field `authData.provider.id` are incorrectly transformed to `_auth_data_provider.id` for custom classes ([#9932](https://github.com/parse-community/parse-server/issues/9932)) ([7b9fa18](https://github.com/parse-community/parse-server/commit/7b9fa18f968ec084ea0b35dad2b5ba0451d59787))
+
+# [8.5.0-alpha.9](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.8...8.5.0-alpha.9) (2025-11-17)
+
+
+### Bug Fixes
+
+* Race condition can cause multiple Apollo server initializations under load ([#9929](https://github.com/parse-community/parse-server/issues/9929)) ([7d5e9fc](https://github.com/parse-community/parse-server/commit/7d5e9fcf3ceb0abad8ab49c75bc26f521a0f1bde))
+
+# [8.5.0-alpha.8](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.7...8.5.0-alpha.8) (2025-11-17)
+
+
+### Performance Improvements
+
+* `Parse.Query.include` now fetches pointers at same level in parallel ([#9861](https://github.com/parse-community/parse-server/issues/9861)) ([dafea21](https://github.com/parse-community/parse-server/commit/dafea21eb39b0fdc2b52bb8a14f7b61e3f2b8d13))
+
+# [8.5.0-alpha.7](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.6...8.5.0-alpha.7) (2025-11-08)
+
+
+### Performance Improvements
+
+* Upgrade MongoDB driver to 6.20.0 ([#9887](https://github.com/parse-community/parse-server/issues/9887)) ([3c9af48](https://github.com/parse-community/parse-server/commit/3c9af48edd999158443b797e388e29495953799e))
+
+# [8.5.0-alpha.6](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.5...8.5.0-alpha.6) (2025-11-08)
+
+
+### Bug Fixes
+
+* `GridFSBucketAdapter` throws when using some Parse Server specific options in MongoDB database options ([#9915](https://github.com/parse-community/parse-server/issues/9915)) ([d3d4003](https://github.com/parse-community/parse-server/commit/d3d4003570b9872f2b0f5a25fc06ce4c4132860d))
+
+# [8.5.0-alpha.5](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.4...8.5.0-alpha.5) (2025-11-08)
+
+
+### Features
+
+* Add Parse Server option `allowPublicExplain` to allow `Parse.Query.explain` without master key ([#9890](https://github.com/parse-community/parse-server/issues/9890)) ([4456b02](https://github.com/parse-community/parse-server/commit/4456b02280c2d8dd58b7250e9e67f1a8647b3452))
+
+# [8.5.0-alpha.4](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.3...8.5.0-alpha.4) (2025-11-08)
+
+
+### Features
+
+* Add MongoDB client event logging via database option `logClientEvents` ([#9914](https://github.com/parse-community/parse-server/issues/9914)) ([b760733](https://github.com/parse-community/parse-server/commit/b760733b98bcfc9c09ac9780066602e1fda108fe))
+
+# [8.5.0-alpha.3](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.2...8.5.0-alpha.3) (2025-11-07)
+
+
+### Features
+
+* Add support for more MongoDB driver options ([#9911](https://github.com/parse-community/parse-server/issues/9911)) ([cff451e](https://github.com/parse-community/parse-server/commit/cff451eabdc380affa600ed711de66f7bd1d00aa))
+
+# [8.5.0-alpha.2](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.1...8.5.0-alpha.2) (2025-11-07)
+
+
+### Features
+
+* Add support for MongoDB driver options `serverSelectionTimeoutMS`, `maxIdleTimeMS`, `heartbeatFrequencyMS` ([#9910](https://github.com/parse-community/parse-server/issues/9910)) ([1b661e9](https://github.com/parse-community/parse-server/commit/1b661e98c86a1db79e076a7297cd9199a72ae1ac))
+
+# [8.5.0-alpha.1](https://github.com/parse-community/parse-server/compare/8.4.0...8.5.0-alpha.1) (2025-11-07)
+
+
+### Features
+
+* Allow option `publicServerURL` to be set dynamically as asynchronous function ([#9803](https://github.com/parse-community/parse-server/issues/9803)) ([460a65c](https://github.com/parse-community/parse-server/commit/460a65cf612f4c86af8038cafcc7e7ffe9eb8440))
+
# [8.4.0-alpha.2](https://github.com/parse-community/parse-server/compare/8.4.0-alpha.1...8.4.0-alpha.2) (2025-11-05)
diff --git a/ci/nodeEngineCheck.js b/ci/nodeEngineCheck.js
index 4023353e17..65a806f760 100644
--- a/ci/nodeEngineCheck.js
+++ b/ci/nodeEngineCheck.js
@@ -86,7 +86,7 @@ class NodeEngineCheck {
file: file,
nodeVersion: version
});
- } catch(e) {
+ } catch {
// eslint-disable-next-line no-console
console.log(`Ignoring file because it is not valid JSON: ${file}`);
core.warning(`Ignoring file because it is not valid JSON: ${file}`);
diff --git a/eslint.config.js b/eslint.config.js
index d1cbac6e5e..3c5bd6806e 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,6 +1,8 @@
const js = require("@eslint/js");
const babelParser = require("@babel/eslint-parser");
const globals = require("globals");
+const unusedImports = require("eslint-plugin-unused-imports");
+
module.exports = [
{
ignores: ["**/lib/**", "**/coverage/**", "**/out/**", "**/types/**"],
@@ -19,8 +21,13 @@ module.exports = [
requireConfigFile: false,
},
},
+ plugins: {
+ "unused-imports": unusedImports,
+ },
rules: {
indent: ["error", 2, { SwitchCase: 1 }],
+ "unused-imports/no-unused-imports": "error",
+ "unused-imports/no-unused-vars": "error",
"linebreak-style": ["error", "unix"],
"no-trailing-spaces": "error",
"eol-last": "error",
diff --git a/jsdoc-conf.json b/jsdoc-conf.json
index b410d239b0..e90f82556b 100644
--- a/jsdoc-conf.json
+++ b/jsdoc-conf.json
@@ -30,7 +30,7 @@
"theme_opts": {
"default_theme": "dark",
"title": "
",
- "create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }"
+ "create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px; height: auto; max-width: 100%; object-fit: contain; }"
}
},
"markdown": {
diff --git a/package-lock.json b/package-lock.json
index 5c36e13703..7ff3ddab63 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,16 @@
{
"name": "parse-server",
- "version": "8.4.0",
+ "version": "8.5.0-alpha.18",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "parse-server",
- "version": "8.4.0",
+ "version": "8.5.0-alpha.18",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@apollo/server": "4.12.1",
- "@babel/eslint-parser": "7.28.0",
"@graphql-tools/merge": "9.0.24",
"@graphql-tools/schema": "10.0.23",
"@graphql-tools/utils": "10.8.6",
@@ -27,7 +26,6 @@
"graphql": "16.11.0",
"graphql-list-fields": "2.0.4",
"graphql-relay": "0.10.2",
- "graphql-tag": "2.12.6",
"graphql-upload": "15.0.2",
"intersect": "1.0.1",
"jsonwebtoken": "9.0.2",
@@ -36,10 +34,10 @@
"lodash": "4.17.21",
"lru-cache": "10.4.0",
"mime": "4.0.7",
- "mongodb": "6.17.0",
+ "mongodb": "6.20.0",
"mustache": "4.2.0",
"otpauth": "9.4.0",
- "parse": "7.0.1",
+ "parse": "7.1.2",
"path-to-regexp": "6.3.0",
"pg-monitor": "3.0.0",
"pg-promise": "12.2.0",
@@ -47,7 +45,6 @@
"punycode": "2.3.1",
"rate-limit-redis": "4.2.0",
"redis": "4.7.0",
- "router": "2.2.0",
"semver": "7.7.2",
"subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0",
@@ -64,6 +61,7 @@
"@apollo/client": "3.13.8",
"@babel/cli": "7.27.0",
"@babel/core": "7.27.4",
+ "@babel/eslint-parser": "7.28.0",
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-transform-flow-strip-types": "7.26.5",
"@babel/preset-env": "7.27.2",
@@ -82,11 +80,10 @@
"deep-diff": "1.0.2",
"eslint": "9.27.0",
"eslint-plugin-expect-type": "0.6.2",
- "flow-bin": "0.271.0",
+ "eslint-plugin-unused-imports": "4.3.0",
"form-data": "4.0.4",
"globals": "16.2.0",
"graphql-tag": "2.12.6",
- "husky": "9.1.7",
"jasmine": "5.7.1",
"jasmine-spec-reporter": "7.0.0",
"jsdoc": "4.0.4",
@@ -155,6 +152,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
+ "dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.1.0",
"@jridgewell/trace-mapping": "^0.3.9"
@@ -837,6 +835,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
@@ -851,6 +850,7 @@
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz",
"integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -860,6 +860,7 @@
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
@@ -889,12 +890,14 @@
"node_modules/@babel/core/node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -903,6 +906,7 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz",
"integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
@@ -921,6 +925,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -929,6 +934,7 @@
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz",
"integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.27.3",
@@ -945,6 +951,7 @@
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
+ "dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@@ -971,6 +978,7 @@
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.27.2",
@@ -987,6 +995,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
"dependencies": {
"yallist": "^3.0.2"
}
@@ -995,6 +1004,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -1002,7 +1012,8 @@
"node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
},
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.27.1",
@@ -1097,6 +1108,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
@@ -1110,6 +1122,7 @@
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
"integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.27.1",
@@ -1200,6 +1213,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1209,6 +1223,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1218,6 +1233,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1242,6 +1258,7 @@
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
@@ -1255,6 +1272,7 @@
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.27.3"
@@ -2517,6 +2535,7 @@
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -2531,6 +2550,7 @@
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -2549,6 +2569,7 @@
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
"engines": {
"node": ">=4"
}
@@ -2557,6 +2578,7 @@
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -2631,6 +2653,7 @@
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
@@ -2649,6 +2672,7 @@
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -2660,6 +2684,7 @@
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
@@ -2669,6 +2694,7 @@
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^2.1.6",
@@ -2683,6 +2709,7 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz",
"integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==",
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2692,6 +2719,7 @@
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
@@ -2704,6 +2732,7 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
@@ -2727,6 +2756,7 @@
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -2739,6 +2769,7 @@
"version": "9.27.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
"integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2751,6 +2782,7 @@
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2760,6 +2792,7 @@
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
+ "dev": true,
"dependencies": {
"@eslint/core": "^0.15.1",
"levn": "^0.4.1"
@@ -2772,6 +2805,7 @@
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
+ "dev": true,
"dependencies": {
"@types/json-schema": "^7.0.15"
},
@@ -3189,6 +3223,7 @@
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
@@ -3198,6 +3233,7 @@
"version": "0.16.6",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
"integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.1",
@@ -3211,6 +3247,7 @@
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
"integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
@@ -3224,6 +3261,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
"engines": {
"node": ">=12.22"
},
@@ -3236,6 +3274,7 @@
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz",
"integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==",
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
@@ -3380,9 +3419,9 @@
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
@@ -3453,6 +3492,7 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
"integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
+ "dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.0.0",
"@jridgewell/sourcemap-codec": "^1.4.10"
@@ -3465,6 +3505,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+ "dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -3473,6 +3514,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -3504,12 +3546,14 @@
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -3706,6 +3750,7 @@
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
"integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==",
+ "dev": true,
"dependencies": {
"eslint-scope": "5.1.1"
}
@@ -6321,6 +6366,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@types/express": {
@@ -6361,6 +6407,7 @@
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
@@ -7428,6 +7475,7 @@
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -7440,6 +7488,7 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -7652,7 +7701,8 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
},
"node_modules/argv-formatter": {
"version": "1.0.0",
@@ -7848,7 +7898,8 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
},
"node_modules/base64-js": {
"version": "1.5.1",
@@ -7991,6 +8042,7 @@
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -8012,6 +8064,7 @@
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
+ "dev": true,
"funding": [
{
"type": "opencollective",
@@ -8264,6 +8317,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
"engines": {
"node": ">=6"
}
@@ -8291,6 +8345,7 @@
"version": "1.0.30001707",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
+ "dev": true,
"funding": [
{
"type": "opencollective",
@@ -8895,7 +8950,8 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
},
"node_modules/config-chain": {
"version": "1.1.13",
@@ -9094,6 +9150,7 @@
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.1"
},
@@ -9122,6 +9179,7 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -9134,8 +9192,7 @@
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
- "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
- "optional": true
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/crypto-random-string": {
"version": "4.0.0",
@@ -9446,7 +9503,8 @@
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
},
"node_modules/deepcopy": {
"version": "2.1.0",
@@ -9823,6 +9881,7 @@
"version": "1.5.129",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz",
"integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==",
+ "dev": true,
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -10119,6 +10178,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "devOptional": true,
"engines": {
"node": ">=6"
}
@@ -10171,6 +10231,7 @@
"version": "9.27.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz",
"integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
@@ -10246,10 +10307,27 @@
"typescript": ">=4"
}
},
+ "node_modules/eslint-plugin-unused-imports": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.3.0.tgz",
+ "integrity": "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0",
+ "eslint": "^9.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@typescript-eslint/eslint-plugin": {
+ "optional": true
+ }
+ }
+ },
"node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
@@ -10262,6 +10340,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true,
"engines": {
"node": ">=10"
}
@@ -10270,6 +10349,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -10284,6 +10364,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -10299,6 +10380,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -10309,12 +10391,14 @@
"node_modules/eslint/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
},
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
"engines": {
"node": ">=10"
},
@@ -10326,6 +10410,7 @@
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
+ "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
@@ -10342,6 +10427,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -10354,6 +10440,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -10363,6 +10450,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
"dependencies": {
"is-glob": "^4.0.3"
},
@@ -10374,6 +10462,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -10382,6 +10471,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -10393,6 +10483,7 @@
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.14.0",
@@ -10410,6 +10501,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -10435,6 +10527,7 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
"dependencies": {
"estraverse": "^5.1.0"
},
@@ -10446,6 +10539,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
"engines": {
"node": ">=4.0"
}
@@ -10454,6 +10548,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -10465,6 +10560,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
"engines": {
"node": ">=4.0"
}
@@ -10473,6 +10569,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
"engines": {
"node": ">=4.0"
}
@@ -10487,6 +10584,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -10764,7 +10862,8 @@
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
},
"node_modules/fast-xml-parser": {
"version": "4.5.3",
@@ -10894,6 +10993,7 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^4.0.0"
@@ -11106,6 +11206,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
@@ -11174,6 +11275,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"flatted": "^3.2.9",
@@ -11187,20 +11289,8 @@
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
- "license": "ISC"
- },
- "node_modules/flow-bin": {
- "version": "0.271.0",
- "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.271.0.tgz",
- "integrity": "sha512-BQjk0DenuPLbB/WlpQzDkSnObOPdzR+PBDItZlawApH/56fqYlM40WuBLs+cfUjjaByML46WHyOAWlQoWnPnjQ==",
"dev": true,
- "license": "MIT",
- "bin": {
- "flow": "cli.js"
- },
- "engines": {
- "node": ">=0.10.0"
- }
+ "license": "ISC"
},
"node_modules/fn.name": {
"version": "1.1.0",
@@ -11669,6 +11759,7 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -12574,21 +12665,6 @@
"node": ">=10.17.0"
}
},
- "node_modules/husky": {
- "version": "9.1.7",
- "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
- "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
- "dev": true,
- "bin": {
- "husky": "bin.js"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/typicode"
- }
- },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -12630,6 +12706,7 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
"engines": {
"node": ">= 4"
}
@@ -12638,6 +12715,7 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -12676,6 +12754,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
"engines": {
"node": ">=0.8.19"
}
@@ -12807,6 +12886,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12823,6 +12903,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
@@ -12975,7 +13056,8 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
},
"node_modules/isstream": {
"version": "0.1.2",
@@ -13310,12 +13392,14 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -13401,6 +13485,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+ "dev": true,
"bin": {
"jsesc": "bin/jsesc"
},
@@ -13419,7 +13504,8 @@
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
},
"node_modules/json-parse-better-errors": {
"version": "1.0.2",
@@ -13446,7 +13532,8 @@
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
@@ -13457,6 +13544,7 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
"bin": {
"json5": "lib/cli.js"
},
@@ -13594,6 +13682,7 @@
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
"dependencies": {
"json-buffer": "3.0.1"
}
@@ -13646,6 +13735,7 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
@@ -13914,6 +14004,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
"dependencies": {
"p-locate": "^5.0.0"
},
@@ -14004,7 +14095,8 @@
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
},
"node_modules/lodash.once": {
"version": "4.1.1",
@@ -14825,6 +14917,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -14950,14 +15043,13 @@
}
},
"node_modules/mongodb": {
- "version": "6.17.0",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz",
- "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==",
- "license": "Apache-2.0",
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
+ "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
"dependencies": {
- "@mongodb-js/saslprep": "^1.1.9",
+ "@mongodb-js/saslprep": "^1.3.0",
"bson": "^6.10.4",
- "mongodb-connection-string-url": "^3.0.0"
+ "mongodb-connection-string-url": "^3.0.2"
},
"engines": {
"node": ">=16.20.1"
@@ -14968,7 +15060,7 @@
"gcp-metadata": "^5.2.0",
"kerberos": "^2.0.1",
"mongodb-client-encryption": ">=6.0.0 <7",
- "snappy": "^7.2.2",
+ "snappy": "^7.3.2",
"socks": "^2.7.1"
},
"peerDependenciesMeta": {
@@ -14996,12 +15088,12 @@
}
},
"node_modules/mongodb-connection-string-url": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz",
- "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
+ "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
"dependencies": {
"@types/whatwg-url": "^11.0.2",
- "whatwg-url": "^13.0.0"
+ "whatwg-url": "^14.1.0 || ^13.0.0"
}
},
"node_modules/mongodb-download-url": {
@@ -15235,7 +15327,8 @@
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
},
"node_modules/negotiator": {
"version": "0.6.3",
@@ -15362,6 +15455,7 @@
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true,
"license": "MIT"
},
"node_modules/node-source-walk": {
@@ -18325,6 +18419,7 @@
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
@@ -18504,6 +18599,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "devOptional": true,
"dependencies": {
"yocto-queue": "^0.1.0"
},
@@ -18518,6 +18614,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
"dependencies": {
"p-limit": "^3.0.2"
},
@@ -18581,6 +18678,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
"dependencies": {
"callsites": "^3.0.0"
},
@@ -18589,22 +18687,19 @@
}
},
"node_modules/parse": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/parse/-/parse-7.0.1.tgz",
- "integrity": "sha512-6hCnE8EWky/MqDtlpMnztzL0BEEsU3jVI7iKl2+AlJeSAeWkCgkPcb30eBNq57FcCnqWWC6uVJAaUMmX3+zrvg==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse/-/parse-7.1.2.tgz",
+ "integrity": "sha512-PXcPZDElBni06WPMxg0e6XmvgYBu3v39pRezZDbsomi8y9k1uNEDO/uICIqndw8jdES2ZfVpHp0TQoar2SObHQ==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime-corejs3": "7.28.4",
+ "crypto-js": "4.2.0",
"idb-keyval": "6.2.2",
"react-native-crypto-js": "1.0.0",
- "uuid": "10.0.0",
"ws": "8.18.3"
},
"engines": {
- "node": "18 || 19 || 20 || 22"
- },
- "optionalDependencies": {
- "crypto-js": "4.2.0"
+ "node": "18 || 19 || 20 || 22 || 24"
}
},
"node_modules/parse-json": {
@@ -18640,19 +18735,6 @@
"integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==",
"license": "Apache-2.0"
},
- "node_modules/parse/node_modules/uuid": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
- "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
- "funding": [
- "https://github.com/sponsors/broofa",
- "https://github.com/sponsors/ctavan"
- ],
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
"node_modules/parse/node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
@@ -18720,6 +18802,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -18737,6 +18820,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -19239,6 +19323,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
"engines": {
"node": ">= 0.8.0"
}
@@ -19934,6 +20019,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
"engines": {
"node": ">=4"
}
@@ -20718,6 +20804,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -20729,6 +20816,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -21312,6 +21400,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
"engines": {
"node": ">=8"
},
@@ -21992,6 +22081,7 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
"dependencies": {
"prelude-ls": "^1.2.1"
},
@@ -22262,6 +22352,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
+ "dev": true,
"funding": [
{
"type": "opencollective",
@@ -22545,6 +22636,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
@@ -22678,6 +22770,7 @@
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -22981,6 +23074,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "devOptional": true,
"engines": {
"node": ">=10"
},
@@ -23063,6 +23157,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
+ "dev": true,
"requires": {
"@jridgewell/gen-mapping": "^0.1.0",
"@jridgewell/trace-mapping": "^0.3.9"
@@ -23559,6 +23654,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
@@ -23568,12 +23664,14 @@
"@babel/compat-data": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz",
- "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="
+ "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==",
+ "dev": true
},
"@babel/core": {
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
+ "dev": true,
"requires": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -23595,12 +23693,14 @@
"convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
},
"semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true
}
}
},
@@ -23608,6 +23708,7 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz",
"integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==",
+ "dev": true,
"requires": {
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
"eslint-visitor-keys": "^2.1.0",
@@ -23617,7 +23718,8 @@
"semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true
}
}
},
@@ -23625,6 +23727,7 @@
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz",
"integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==",
+ "dev": true,
"requires": {
"@babel/parser": "^7.27.3",
"@babel/types": "^7.27.3",
@@ -23637,6 +23740,7 @@
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
+ "dev": true,
"requires": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@@ -23658,6 +23762,7 @@
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
"requires": {
"@babel/compat-data": "^7.27.2",
"@babel/helper-validator-option": "^7.27.1",
@@ -23670,6 +23775,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
"requires": {
"yallist": "^3.0.2"
}
@@ -23677,12 +23783,14 @@
"semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
}
}
},
@@ -23755,6 +23863,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
"requires": {
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.1"
@@ -23764,6 +23873,7 @@
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
"integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
+ "dev": true,
"requires": {
"@babel/helper-module-imports": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1",
@@ -23820,17 +23930,20 @@
"@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
- "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true
},
"@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
- "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true
},
"@babel/helper-validator-option": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
- "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true
},
"@babel/helper-wrap-function": {
"version": "7.27.1",
@@ -23847,6 +23960,7 @@
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
+ "dev": true,
"requires": {
"@babel/template": "^7.27.2",
"@babel/types": "^7.27.6"
@@ -23856,6 +23970,7 @@
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
+ "dev": true,
"requires": {
"@babel/types": "^7.27.3"
}
@@ -24632,6 +24747,7 @@
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
"requires": {
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
@@ -24642,6 +24758,7 @@
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
+ "dev": true,
"requires": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.3",
@@ -24655,7 +24772,8 @@
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true
}
}
},
@@ -24663,6 +24781,7 @@
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
+ "dev": true,
"requires": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
@@ -24727,6 +24846,7 @@
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
"requires": {
"eslint-visitor-keys": "^3.4.3"
},
@@ -24734,19 +24854,22 @@
"eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true
}
}
},
"@eslint-community/regexpp": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
- "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true
},
"@eslint/config-array": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
+ "dev": true,
"requires": {
"@eslint/object-schema": "^2.1.6",
"debug": "^4.3.1",
@@ -24756,12 +24879,14 @@
"@eslint/config-helpers": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz",
- "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="
+ "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==",
+ "dev": true
},
"@eslint/core": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
+ "dev": true,
"requires": {
"@types/json-schema": "^7.0.15"
}
@@ -24770,6 +24895,7 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
@@ -24785,24 +24911,28 @@
"globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
- "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true
}
}
},
"@eslint/js": {
"version": "9.27.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
- "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA=="
+ "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
+ "dev": true
},
"@eslint/object-schema": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
- "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true
},
"@eslint/plugin-kit": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
+ "dev": true,
"requires": {
"@eslint/core": "^0.15.1",
"levn": "^0.4.1"
@@ -24812,6 +24942,7 @@
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
+ "dev": true,
"requires": {
"@types/json-schema": "^7.0.15"
}
@@ -25108,12 +25239,14 @@
"@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
- "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true
},
"@humanfs/node": {
"version": "0.16.6",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
"integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
"requires": {
"@humanfs/core": "^0.19.1",
"@humanwhocodes/retry": "^0.3.0"
@@ -25122,19 +25255,22 @@
"@humanwhocodes/retry": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
- "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true
}
}
},
"@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
- "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true
},
"@humanwhocodes/retry": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz",
- "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="
+ "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==",
+ "dev": true
},
"@isaacs/cliui": {
"version": "8.0.2",
@@ -25234,9 +25370,9 @@
}
},
"js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
@@ -25288,6 +25424,7 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
"integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
+ "dev": true,
"requires": {
"@jridgewell/set-array": "^1.0.0",
"@jridgewell/sourcemap-codec": "^1.4.10"
@@ -25296,12 +25433,14 @@
"@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
- "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
+ "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+ "dev": true
},
"@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true
},
"@jridgewell/source-map": {
"version": "0.3.6",
@@ -25329,12 +25468,14 @@
"@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true
},
"@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
"requires": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -25510,6 +25651,7 @@
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
"integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==",
+ "dev": true,
"requires": {
"eslint-scope": "5.1.1"
}
@@ -27303,7 +27445,8 @@
"@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
- "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true
},
"@types/express": {
"version": "4.17.21",
@@ -27341,7 +27484,8 @@
"@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true
},
"@types/jsonwebtoken": {
"version": "9.0.5",
@@ -28067,12 +28211,14 @@
"acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
- "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "dev": true
},
"acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
"requires": {}
},
"agent-base": {
@@ -28228,7 +28374,8 @@
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
},
"argv-formatter": {
"version": "1.0.0",
@@ -28384,7 +28531,8 @@
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
},
"base64-js": {
"version": "1.5.1",
@@ -28493,6 +28641,7 @@
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -28511,6 +28660,7 @@
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
+ "dev": true,
"requires": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@@ -28673,7 +28823,8 @@
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true
},
"camel-case": {
"version": "4.1.2",
@@ -28694,7 +28845,8 @@
"caniuse-lite": {
"version": "1.0.30001707",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
- "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw=="
+ "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
+ "dev": true
},
"cardinal": {
"version": "2.1.1",
@@ -29123,7 +29275,8 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
},
"config-chain": {
"version": "1.1.13",
@@ -29274,6 +29427,7 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -29283,8 +29437,7 @@
"crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
- "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
- "optional": true
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"crypto-random-string": {
"version": "4.0.0",
@@ -29505,7 +29658,8 @@
"deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
},
"deepcopy": {
"version": "2.1.0",
@@ -29790,7 +29944,8 @@
"electron-to-chromium": {
"version": "1.5.129",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz",
- "integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA=="
+ "integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==",
+ "dev": true
},
"emoji-regex": {
"version": "8.0.0",
@@ -29992,7 +30147,8 @@
"escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "devOptional": true
},
"escape-html": {
"version": "1.0.3",
@@ -30029,6 +30185,7 @@
"version": "9.27.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz",
"integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==",
+ "dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -30071,6 +30228,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
"requires": {
"color-convert": "^2.0.1"
}
@@ -30079,6 +30237,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -30088,6 +30247,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
"requires": {
"color-name": "~1.1.4"
}
@@ -30095,17 +30255,20 @@
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
},
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true
},
"eslint-scope": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
+ "dev": true,
"requires": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
@@ -30114,17 +30277,20 @@
"eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
- "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true
},
"estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true
},
"glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
"requires": {
"is-glob": "^4.0.3"
}
@@ -30132,12 +30298,14 @@
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
"requires": {
"has-flag": "^4.0.0"
}
@@ -30155,10 +30323,18 @@
"get-tsconfig": "^4.8.1"
}
},
+ "eslint-plugin-unused-imports": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.3.0.tgz",
+ "integrity": "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==",
+ "dev": true,
+ "requires": {}
+ },
"eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
"requires": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
@@ -30167,12 +30343,14 @@
"eslint-visitor-keys": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
- "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw=="
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true
},
"espree": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "dev": true,
"requires": {
"acorn": "^8.14.0",
"acorn-jsx": "^5.3.2",
@@ -30182,7 +30360,8 @@
"eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
- "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true
}
}
},
@@ -30196,6 +30375,7 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
"requires": {
"estraverse": "^5.1.0"
},
@@ -30203,7 +30383,8 @@
"estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true
}
}
},
@@ -30211,6 +30392,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
"requires": {
"estraverse": "^5.2.0"
},
@@ -30218,14 +30400,16 @@
"estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true
}
}
},
"estraverse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true
},
"estree-walker": {
"version": "2.0.2",
@@ -30236,7 +30420,8 @@
"esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
},
"etag": {
"version": "1.8.1",
@@ -30429,7 +30614,8 @@
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
},
"fast-xml-parser": {
"version": "4.5.3",
@@ -30515,6 +30701,7 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
"requires": {
"flat-cache": "^4.0.0"
}
@@ -30666,6 +30853,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
"requires": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
@@ -30710,6 +30898,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
"requires": {
"flatted": "^3.2.9",
"keyv": "^4.5.4"
@@ -30718,12 +30907,7 @@
"flatted": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
- "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="
- },
- "flow-bin": {
- "version": "0.271.0",
- "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.271.0.tgz",
- "integrity": "sha512-BQjk0DenuPLbB/WlpQzDkSnObOPdzR+PBDItZlawApH/56fqYlM40WuBLs+cfUjjaByML46WHyOAWlQoWnPnjQ==",
+ "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
"dev": true
},
"fn.name": {
@@ -31042,7 +31226,8 @@
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true
},
"get-amd-module-type": {
"version": "6.0.0",
@@ -31688,12 +31873,6 @@
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"dev": true
},
- "husky": {
- "version": "9.1.7",
- "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
- "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
- "dev": true
- },
"iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -31716,12 +31895,14 @@
"ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
- "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true
},
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
"requires": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -31746,7 +31927,8 @@
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true
},
"indent-string": {
"version": "4.0.0",
@@ -31841,7 +32023,8 @@
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true
},
"is-fullwidth-code-point": {
"version": "3.0.0",
@@ -31852,6 +32035,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
@@ -31952,7 +32136,8 @@
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
},
"isstream": {
"version": "0.1.2",
@@ -32205,12 +32390,14 @@
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
},
"js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
"requires": {
"argparse": "^2.0.1"
}
@@ -32279,7 +32466,8 @@
"jsesc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
- "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="
+ "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+ "dev": true
},
"json-bigint": {
"version": "1.0.0",
@@ -32292,7 +32480,8 @@
"json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
},
"json-parse-better-errors": {
"version": "1.0.2",
@@ -32319,7 +32508,8 @@
"json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
},
"json-stringify-safe": {
"version": "5.0.1",
@@ -32329,7 +32519,8 @@
"json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true
},
"jsonfile": {
"version": "6.1.0",
@@ -32438,6 +32629,7 @@
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
"requires": {
"json-buffer": "3.0.1"
}
@@ -32490,6 +32682,7 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
"requires": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
@@ -32673,6 +32866,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
"requires": {
"p-locate": "^5.0.0"
}
@@ -32756,7 +32950,8 @@
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
},
"lodash.once": {
"version": "4.1.1",
@@ -33306,6 +33501,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -33390,22 +33586,22 @@
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"mongodb": {
- "version": "6.17.0",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz",
- "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==",
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
+ "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
"requires": {
- "@mongodb-js/saslprep": "^1.1.9",
+ "@mongodb-js/saslprep": "^1.3.0",
"bson": "^6.10.4",
- "mongodb-connection-string-url": "^3.0.0"
+ "mongodb-connection-string-url": "^3.0.2"
}
},
"mongodb-connection-string-url": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz",
- "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
+ "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
"requires": {
"@types/whatwg-url": "^11.0.2",
- "whatwg-url": "^13.0.0"
+ "whatwg-url": "^14.1.0 || ^13.0.0"
}
},
"mongodb-download-url": {
@@ -33572,7 +33768,8 @@
"natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
},
"negotiator": {
"version": "0.6.3",
@@ -33660,7 +33857,8 @@
"node-releases": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
- "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true
},
"node-source-walk": {
"version": "7.0.0",
@@ -35668,6 +35866,7 @@
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
"requires": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
@@ -35792,6 +35991,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "devOptional": true,
"requires": {
"yocto-queue": "^0.1.0"
}
@@ -35800,6 +36000,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
"requires": {
"p-limit": "^3.0.2"
}
@@ -35848,20 +36049,20 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
"requires": {
"callsites": "^3.0.0"
}
},
"parse": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/parse/-/parse-7.0.1.tgz",
- "integrity": "sha512-6hCnE8EWky/MqDtlpMnztzL0BEEsU3jVI7iKl2+AlJeSAeWkCgkPcb30eBNq57FcCnqWWC6uVJAaUMmX3+zrvg==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse/-/parse-7.1.2.tgz",
+ "integrity": "sha512-PXcPZDElBni06WPMxg0e6XmvgYBu3v39pRezZDbsomi8y9k1uNEDO/uICIqndw8jdES2ZfVpHp0TQoar2SObHQ==",
"requires": {
"@babel/runtime-corejs3": "7.28.4",
"crypto-js": "4.2.0",
"idb-keyval": "6.2.2",
"react-native-crypto-js": "1.0.0",
- "uuid": "10.0.0",
"ws": "8.18.3"
},
"dependencies": {
@@ -35870,11 +36071,6 @@
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz",
"integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="
},
- "uuid": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
- "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
- },
"ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
@@ -35942,7 +36138,8 @@
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
},
"path-is-absolute": {
"version": "1.0.1",
@@ -35953,7 +36150,8 @@
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true
},
"path-parse": {
"version": "1.0.7",
@@ -36294,7 +36492,8 @@
"prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true
},
"prettier": {
"version": "2.0.5",
@@ -36810,7 +37009,8 @@
"resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true
},
"resolve-pkg-maps": {
"version": "1.0.0",
@@ -37329,6 +37529,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
"requires": {
"shebang-regex": "^3.0.0"
}
@@ -37336,7 +37537,8 @@
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true
},
"showdown": {
"version": "2.1.0",
@@ -37781,7 +37983,8 @@
"strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
- "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true
},
"strnum": {
"version": "1.1.2",
@@ -38258,6 +38461,7 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
"requires": {
"prelude-ls": "^1.2.1"
}
@@ -38438,6 +38642,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
+ "dev": true,
"requires": {
"escalade": "^3.2.0",
"picocolors": "^1.1.0"
@@ -38643,6 +38848,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
"requires": {
"isexe": "^2.0.0"
}
@@ -38746,7 +38952,8 @@
"word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
- "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true
},
"wordwrap": {
"version": "1.0.0",
@@ -38971,7 +39178,8 @@
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "devOptional": true
},
"yoctocolors": {
"version": "2.1.1",
diff --git a/package.json b/package.json
index 4d8743c10a..163f13fb7b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "parse-server",
- "version": "8.4.0",
+ "version": "8.5.0-alpha.18",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -21,7 +21,6 @@
"license": "Apache-2.0",
"dependencies": {
"@apollo/server": "4.12.1",
- "@babel/eslint-parser": "7.28.0",
"@graphql-tools/merge": "9.0.24",
"@graphql-tools/schema": "10.0.23",
"@graphql-tools/utils": "10.8.6",
@@ -37,7 +36,6 @@
"graphql": "16.11.0",
"graphql-list-fields": "2.0.4",
"graphql-relay": "0.10.2",
- "graphql-tag": "2.12.6",
"graphql-upload": "15.0.2",
"intersect": "1.0.1",
"jsonwebtoken": "9.0.2",
@@ -46,10 +44,10 @@
"lodash": "4.17.21",
"lru-cache": "10.4.0",
"mime": "4.0.7",
- "mongodb": "6.17.0",
+ "mongodb": "6.20.0",
"mustache": "4.2.0",
"otpauth": "9.4.0",
- "parse": "7.0.1",
+ "parse": "7.1.2",
"path-to-regexp": "6.3.0",
"pg-monitor": "3.0.0",
"pg-promise": "12.2.0",
@@ -57,7 +55,6 @@
"punycode": "2.3.1",
"rate-limit-redis": "4.2.0",
"redis": "4.7.0",
- "router": "2.2.0",
"semver": "7.7.2",
"subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0",
@@ -71,6 +68,7 @@
"@apollo/client": "3.13.8",
"@babel/cli": "7.27.0",
"@babel/core": "7.27.4",
+ "@babel/eslint-parser": "7.28.0",
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-transform-flow-strip-types": "7.26.5",
"@babel/preset-env": "7.27.2",
@@ -89,11 +87,10 @@
"deep-diff": "1.0.2",
"eslint": "9.27.0",
"eslint-plugin-expect-type": "0.6.2",
- "flow-bin": "0.271.0",
+ "eslint-plugin-unused-imports": "4.3.0",
"form-data": "4.0.4",
"globals": "16.2.0",
"graphql-tag": "2.12.6",
- "husky": "9.1.7",
"jasmine": "5.7.1",
"jasmine-spec-reporter": "7.0.0",
"jsdoc": "4.0.4",
@@ -138,7 +135,10 @@
"prettier": "prettier --write {src,spec}/{**/*,*}.js",
"prepare": "npm run build",
"postinstall": "node -p 'require(\"./postinstall.js\")()'",
- "madge:circular": "node_modules/.bin/madge ./src --circular"
+ "madge:circular": "node_modules/.bin/madge ./src --circular",
+ "benchmark": "cross-env MONGODB_VERSION=8.0.4 MONGODB_TOPOLOGY=standalone mongodb-runner exec -t standalone --version 8.0.4 -- --port 27017 -- npm run benchmark:only",
+ "benchmark:only": "node benchmark/performance.js",
+ "benchmark:quick": "cross-env BENCHMARK_ITERATIONS=10 npm run benchmark:only"
},
"types": "types/index.d.ts",
"engines": {
diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js
index 5b9084f863..f4813975f4 100644
--- a/resources/buildConfigDefinitions.js
+++ b/resources/buildConfigDefinitions.js
@@ -36,15 +36,17 @@ const nestedOptionEnvPrefix = {
IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_',
LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_',
+ LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_',
+ LogLevel: 'PARSE_SERVER_LOG_LEVEL_',
+ LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_',
PagesOptions: 'PARSE_SERVER_PAGES_',
PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_',
ParseServerOptions: 'PARSE_SERVER_',
PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_',
- SecurityOptions: 'PARSE_SERVER_SECURITY_',
- SchemaOptions: 'PARSE_SERVER_SCHEMA_',
- LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_',
+ SchemaOptions: 'PARSE_SERVER_SCHEMA_',
+ SecurityOptions: 'PARSE_SERVER_SECURITY_',
};
function last(array) {
diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js
index 1525147a40..f6d6af9393 100644
--- a/spec/AudienceRouter.spec.js
+++ b/spec/AudienceRouter.spec.js
@@ -5,6 +5,13 @@ const request = require('../lib/request');
const AudiencesRouter = require('../lib/Routers/AudiencesRouter').AudiencesRouter;
describe('AudiencesRouter', () => {
+ let loggerErrorSpy;
+
+ beforeEach(() => {
+ const logger = require('../lib/logger').default;
+ loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+ });
+
it('uses find condition from request.body', done => {
const config = Config.get('test');
const androidAudienceRequest = {
@@ -263,55 +270,65 @@ describe('AudiencesRouter', () => {
});
it('should only create with master key', done => {
+ loggerErrorSpy.calls.reset();
Parse._request('POST', 'push_audiences', {
name: 'My Audience',
query: JSON.stringify({ deviceType: 'ios' }),
}).then(
() => {},
error => {
- expect(error.message).toEqual('unauthorized: master key is required');
+ expect(error.message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
done();
}
);
});
it('should only find with master key', done => {
+ loggerErrorSpy.calls.reset();
Parse._request('GET', 'push_audiences', {}).then(
() => {},
error => {
- expect(error.message).toEqual('unauthorized: master key is required');
+ expect(error.message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
done();
}
);
});
it('should only get with master key', done => {
+ loggerErrorSpy.calls.reset();
Parse._request('GET', `push_audiences/someId`, {}).then(
() => {},
error => {
- expect(error.message).toEqual('unauthorized: master key is required');
+ expect(error.message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
done();
}
);
});
it('should only update with master key', done => {
+ loggerErrorSpy.calls.reset();
Parse._request('PUT', `push_audiences/someId`, {
name: 'My Audience 2',
}).then(
() => {},
error => {
- expect(error.message).toEqual('unauthorized: master key is required');
+ expect(error.message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
done();
}
);
});
it('should only delete with master key', done => {
+ loggerErrorSpy.calls.reset();
Parse._request('DELETE', `push_audiences/someId`, {}).then(
() => {},
error => {
- expect(error.message).toEqual('unauthorized: master key is required');
+ expect(error.message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
done();
}
);
diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js
index 308c7731b8..b2aac473b2 100644
--- a/spec/CloudCode.spec.js
+++ b/spec/CloudCode.spec.js
@@ -3307,19 +3307,19 @@ describe('afterFind hooks', () => {
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
expect(() => {
Parse.Cloud.beforeLogin('SomeClass', () => { });
- }).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
+ }).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
expect(() => {
Parse.Cloud.afterLogin(() => { });
- }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
+ }).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
expect(() => {
Parse.Cloud.afterLogin('_User', () => { });
- }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
+ }).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
expect(() => {
Parse.Cloud.afterLogin(Parse.User, () => { });
- }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
+ }).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
expect(() => {
Parse.Cloud.afterLogin('SomeClass', () => { });
- }).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
+ }).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
expect(() => {
Parse.Cloud.afterLogout(() => { });
}).not.toThrow();
@@ -4656,3 +4656,157 @@ describe('sendEmail', () => {
);
});
});
+
+describe('beforePasswordResetRequest hook', () => {
+ it('should run beforePasswordResetRequest with valid user', async () => {
+ let hit = 0;
+ let sendPasswordResetEmailCalled = false;
+ const emailAdapter = {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => {
+ sendPasswordResetEmailCalled = true;
+ },
+ sendMail: () => {},
+ };
+
+ await reconfigureServer({
+ appName: 'test',
+ emailAdapter: emailAdapter,
+ publicServerURL: 'http://localhost:8378/1',
+ });
+
+ Parse.Cloud.beforePasswordResetRequest(req => {
+ hit++;
+ expect(req.object).toBeDefined();
+ expect(req.object.get('email')).toEqual('test@example.com');
+ expect(req.object.get('username')).toEqual('testuser');
+ });
+
+ const user = new Parse.User();
+ user.setUsername('testuser');
+ user.setPassword('password');
+ user.set('email', 'test@example.com');
+ await user.signUp();
+
+ await Parse.User.requestPasswordReset('test@example.com');
+ expect(hit).toBe(1);
+ expect(sendPasswordResetEmailCalled).toBe(true);
+ });
+
+ it('should be able to block password reset request if an error is thrown', async () => {
+ let hit = 0;
+ let sendPasswordResetEmailCalled = false;
+ const emailAdapter = {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => {
+ sendPasswordResetEmailCalled = true;
+ },
+ sendMail: () => {},
+ };
+
+ await reconfigureServer({
+ appName: 'test',
+ emailAdapter: emailAdapter,
+ publicServerURL: 'http://localhost:8378/1',
+ });
+
+ Parse.Cloud.beforePasswordResetRequest(req => {
+ hit++;
+ throw new Error('password reset blocked');
+ });
+
+ const user = new Parse.User();
+ user.setUsername('testuser');
+ user.setPassword('password');
+ user.set('email', 'test@example.com');
+ await user.signUp();
+
+ try {
+ await Parse.User.requestPasswordReset('test@example.com');
+ throw new Error('should not have sent password reset email.');
+ } catch (e) {
+ expect(e.message).toBe('password reset blocked');
+ }
+ expect(hit).toBe(1);
+ expect(sendPasswordResetEmailCalled).toBe(false);
+ });
+
+ it('should not run beforePasswordResetRequest if email does not exist', async () => {
+ let hit = 0;
+ const emailAdapter = {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => {},
+ sendMail: () => {},
+ };
+
+ await reconfigureServer({
+ appName: 'test',
+ emailAdapter: emailAdapter,
+ publicServerURL: 'http://localhost:8378/1',
+ });
+
+ Parse.Cloud.beforePasswordResetRequest(req => {
+ hit++;
+ });
+
+ await Parse.User.requestPasswordReset('nonexistent@example.com');
+
+ expect(hit).toBe(0);
+ });
+
+ it('should have expected data in request in beforePasswordResetRequest', async () => {
+ const emailAdapter = {
+ sendVerificationEmail: () => Promise.resolve(),
+ sendPasswordResetEmail: () => {},
+ sendMail: () => {},
+ };
+
+ await reconfigureServer({
+ appName: 'test',
+ emailAdapter: emailAdapter,
+ publicServerURL: 'http://localhost:8378/1',
+ });
+
+ const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=';
+ const file = new Parse.File('myfile.txt', { base64 });
+ await file.save();
+
+ Parse.Cloud.beforePasswordResetRequest(req => {
+ expect(req.object).toBeDefined();
+ expect(req.object.get('email')).toBeDefined();
+ expect(req.object.get('email')).toBe('test2@example.com');
+ expect(req.object.get('file')).toBeDefined();
+ expect(req.object.get('file')).toBeInstanceOf(Parse.File);
+ expect(req.object.get('file').name()).toContain('myfile.txt');
+ expect(req.headers).toBeDefined();
+ expect(req.ip).toBeDefined();
+ expect(req.installationId).toBeDefined();
+ expect(req.context).toBeDefined();
+ expect(req.config).toBeDefined();
+ });
+
+ const user = new Parse.User();
+ user.setUsername('testuser2');
+ user.setPassword('password');
+ user.set('email', 'test2@example.com');
+ user.set('file', file);
+ await user.signUp();
+
+ await Parse.User.requestPasswordReset('test2@example.com');
+ });
+
+ it('should validate that only _User class is allowed for beforePasswordResetRequest', () => {
+ expect(() => {
+ Parse.Cloud.beforePasswordResetRequest('SomeClass', () => { });
+ }).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
+ expect(() => {
+ Parse.Cloud.beforePasswordResetRequest(() => { });
+ }).not.toThrow();
+ expect(() => {
+ Parse.Cloud.beforePasswordResetRequest('_User', () => { });
+ }).not.toThrow();
+ expect(() => {
+ Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { });
+ }).not.toThrow();
+ });
+});
diff --git a/spec/Deprecator.spec.js b/spec/Deprecator.spec.js
index 3af5d10c31..80e8632014 100644
--- a/spec/Deprecator.spec.js
+++ b/spec/Deprecator.spec.js
@@ -45,4 +45,29 @@ describe('Deprecator', () => {
`DeprecationWarning: ${options.usage} is deprecated and will be removed in a future version. ${options.solution}`
);
});
+
+ it('logs deprecation for nested option key with dot notation', async () => {
+ deprecations = [{ optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' }];
+
+ spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations);
+ const logger = require('../lib/logger').logger;
+ const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
+
+ await reconfigureServer();
+ expect(logSpy.calls.all()[0].args[0]).toEqual(
+ `DeprecationWarning: The Parse Server option '${deprecations[0].optionKey}' default will change to '${deprecations[0].changeNewDefault}' in a future version.`
+ );
+ });
+
+ it('does not log deprecation for nested option key if option is set manually', async () => {
+ deprecations = [{ optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' }];
+
+ spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations);
+ const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});
+ const Config = require('../lib/Config');
+ const config = Config.get('test');
+ // Directly test scanParseServerOptions with nested option set
+ Deprecator.scanParseServerOptions({ databaseOptions: { allowPublicExplain: true } });
+ expect(logSpy).not.toHaveBeenCalled();
+ });
});
diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js
index d30415edf3..8e1e4f2900 100644
--- a/spec/GridFSBucketStorageAdapter.spec.js
+++ b/spec/GridFSBucketStorageAdapter.spec.js
@@ -24,10 +24,20 @@ describe_only_db('mongo')('GridFSBucket', () => {
const databaseURI = 'mongodb://localhost:27017/parse';
const gfsAdapter = new GridFSBucketAdapter(databaseURI, {
retryWrites: true,
- // these are not supported by the mongo client
+ // Parse Server-specific options that should be filtered out before passing to MongoDB client
+ allowPublicExplain: true,
enableSchemaHooks: true,
schemaCacheTtl: 5000,
maxTimeMS: 30000,
+ disableIndexFieldValidation: true,
+ logClientEvents: [{ name: 'commandStarted' }],
+ createIndexUserUsername: true,
+ createIndexUserUsernameCaseInsensitive: true,
+ createIndexUserEmail: true,
+ createIndexUserEmailCaseInsensitive: true,
+ createIndexUserEmailVerifyToken: true,
+ createIndexUserPasswordResetToken: true,
+ createIndexRoleName: true,
});
const db = await gfsAdapter._connect();
diff --git a/spec/LogsRouter.spec.js b/spec/LogsRouter.spec.js
index b25ac25be5..d4b77baaa8 100644
--- a/spec/LogsRouter.spec.js
+++ b/spec/LogsRouter.spec.js
@@ -52,6 +52,9 @@ describe_only(() => {
});
it('can check invalid master key of request', done => {
+ const logger = require('../lib/logger').default;
+ const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+ loggerErrorSpy.calls.reset();
request({
url: 'http://localhost:8378/1/scriptlog',
headers: {
@@ -61,7 +64,8 @@ describe_only(() => {
}).then(fail, response => {
const body = response.data;
expect(response.status).toEqual(403);
- expect(body.error).toEqual('unauthorized: master key is required');
+ expect(body.error).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
done();
});
});
diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js
index 7d0d220cff..8b14973243 100644
--- a/spec/MongoStorageAdapter.spec.js
+++ b/spec/MongoStorageAdapter.spec.js
@@ -824,4 +824,243 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
expect(roleIndexes.find(idx => idx.name === 'name_1')).toBeDefined();
});
});
+
+ describe('logClientEvents', () => {
+ it('should log MongoDB client events when configured', async () => {
+ const logger = require('../lib/logger').logger;
+ const logSpy = spyOn(logger, 'warn');
+
+ const logClientEvents = [
+ {
+ name: 'serverDescriptionChanged',
+ keys: ['address'],
+ logLevel: 'warn',
+ },
+ ];
+
+ const adapter = new MongoStorageAdapter({
+ uri: databaseURI,
+ mongoOptions: { logClientEvents },
+ });
+
+ // Connect to trigger event listeners setup
+ await adapter.connect();
+
+ // Manually trigger the event to test the listener
+ const mockEvent = {
+ address: 'localhost:27017',
+ previousDescription: { type: 'Unknown' },
+ newDescription: { type: 'Standalone' },
+ };
+
+ adapter.client.emit('serverDescriptionChanged', mockEvent);
+
+ // Verify the log was called with the correct message
+ expect(logSpy).toHaveBeenCalledWith(
+ jasmine.stringMatching(/MongoDB client event serverDescriptionChanged:.*"address":"localhost:27017"/)
+ );
+
+ await adapter.handleShutdown();
+ });
+
+ it('should log entire event when keys are not specified', async () => {
+ const logger = require('../lib/logger').logger;
+ const logSpy = spyOn(logger, 'info');
+
+ const logClientEvents = [
+ {
+ name: 'connectionPoolReady',
+ logLevel: 'info',
+ },
+ ];
+
+ const adapter = new MongoStorageAdapter({
+ uri: databaseURI,
+ mongoOptions: { logClientEvents },
+ });
+
+ await adapter.connect();
+
+ const mockEvent = {
+ address: 'localhost:27017',
+ options: { maxPoolSize: 100 },
+ };
+
+ adapter.client.emit('connectionPoolReady', mockEvent);
+
+ expect(logSpy).toHaveBeenCalledWith(
+ jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017".*"options"/)
+ );
+
+ await adapter.handleShutdown();
+ });
+
+ it('should extract nested keys using dot notation', async () => {
+ const logger = require('../lib/logger').logger;
+ const logSpy = spyOn(logger, 'warn');
+
+ const logClientEvents = [
+ {
+ name: 'topologyDescriptionChanged',
+ keys: ['previousDescription.type', 'newDescription.type', 'newDescription.servers.size'],
+ logLevel: 'warn',
+ },
+ ];
+
+ const adapter = new MongoStorageAdapter({
+ uri: databaseURI,
+ mongoOptions: { logClientEvents },
+ });
+
+ await adapter.connect();
+
+ const mockEvent = {
+ topologyId: 1,
+ previousDescription: { type: 'Unknown' },
+ newDescription: {
+ type: 'ReplicaSetWithPrimary',
+ servers: { size: 3 },
+ },
+ };
+
+ adapter.client.emit('topologyDescriptionChanged', mockEvent);
+
+ expect(logSpy).toHaveBeenCalledWith(
+ jasmine.stringMatching(/MongoDB client event topologyDescriptionChanged:.*"previousDescription.type":"Unknown".*"newDescription.type":"ReplicaSetWithPrimary".*"newDescription.servers.size":3/)
+ );
+
+ await adapter.handleShutdown();
+ });
+
+ it('should handle invalid log level gracefully', async () => {
+ const logger = require('../lib/logger').logger;
+ const infoSpy = spyOn(logger, 'info');
+
+ const logClientEvents = [
+ {
+ name: 'connectionPoolReady',
+ keys: ['address'],
+ logLevel: 'invalidLogLevel', // Invalid log level
+ },
+ ];
+
+ const adapter = new MongoStorageAdapter({
+ uri: databaseURI,
+ mongoOptions: { logClientEvents },
+ });
+
+ await adapter.connect();
+
+ const mockEvent = {
+ address: 'localhost:27017',
+ };
+
+ adapter.client.emit('connectionPoolReady', mockEvent);
+
+ // Should fallback to 'info' level
+ expect(infoSpy).toHaveBeenCalledWith(
+ jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017"/)
+ );
+
+ await adapter.handleShutdown();
+ });
+
+ it('should handle Map and Set instances in events', async () => {
+ const logger = require('../lib/logger').logger;
+ const warnSpy = spyOn(logger, 'warn');
+
+ const logClientEvents = [
+ {
+ name: 'customEvent',
+ logLevel: 'warn',
+ },
+ ];
+
+ const adapter = new MongoStorageAdapter({
+ uri: databaseURI,
+ mongoOptions: { logClientEvents },
+ });
+
+ await adapter.connect();
+
+ const mockEvent = {
+ mapData: new Map([['key1', 'value1'], ['key2', 'value2']]),
+ setData: new Set([1, 2, 3]),
+ };
+
+ adapter.client.emit('customEvent', mockEvent);
+
+ // Should serialize Map and Set properly
+ expect(warnSpy).toHaveBeenCalledWith(
+ jasmine.stringMatching(/MongoDB client event customEvent:.*"mapData":\{"key1":"value1","key2":"value2"\}.*"setData":\[1,2,3\]/)
+ );
+
+ await adapter.handleShutdown();
+ });
+
+ it('should handle missing keys in event object', async () => {
+ const logger = require('../lib/logger').logger;
+ const infoSpy = spyOn(logger, 'info');
+
+ const logClientEvents = [
+ {
+ name: 'testEvent',
+ keys: ['nonexistent.nested.key', 'another.missing'],
+ logLevel: 'info',
+ },
+ ];
+
+ const adapter = new MongoStorageAdapter({
+ uri: databaseURI,
+ mongoOptions: { logClientEvents },
+ });
+
+ await adapter.connect();
+
+ const mockEvent = {
+ actualField: 'value',
+ };
+
+ adapter.client.emit('testEvent', mockEvent);
+
+ // Should handle missing keys gracefully with undefined values
+ expect(infoSpy).toHaveBeenCalledWith(
+ jasmine.stringMatching(/MongoDB client event testEvent:/)
+ );
+
+ await adapter.handleShutdown();
+ });
+
+ it('should handle circular references gracefully', async () => {
+ const logger = require('../lib/logger').logger;
+ const infoSpy = spyOn(logger, 'info');
+
+ const logClientEvents = [
+ {
+ name: 'circularEvent',
+ logLevel: 'info',
+ },
+ ];
+
+ const adapter = new MongoStorageAdapter({
+ uri: databaseURI,
+ mongoOptions: { logClientEvents },
+ });
+
+ await adapter.connect();
+
+ // Create circular reference
+ const mockEvent = { name: 'test' };
+ mockEvent.self = mockEvent;
+
+ adapter.client.emit('circularEvent', mockEvent);
+
+ // Should handle circular reference with [Circular] marker
+ expect(infoSpy).toHaveBeenCalledWith(
+ jasmine.stringMatching(/MongoDB client event circularEvent:.*\[Circular\]/)
+ );
+
+ await adapter.handleShutdown();
+ });
+ });
});
diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js
index 60adb4b7f0..96a38f36dd 100644
--- a/spec/MongoTransform.spec.js
+++ b/spec/MongoTransform.spec.js
@@ -521,6 +521,23 @@ describe('parseObjectToMongoObjectForCreate', () => {
expect(output.authData).toBe('random');
done();
});
+
+ it('should only transform authData.provider.id for _User class', () => {
+ // Test that for _User class, authData.facebook.id is transformed
+ const userInput = {
+ 'authData.facebook.id': '10000000000000001',
+ };
+ const userOutput = transform.transformWhere('_User', userInput, { fields: {} });
+ expect(userOutput['_auth_data_facebook.id']).toBe('10000000000000001');
+
+ // Test that for non-User classes, authData.facebook.id is NOT transformed
+ const customInput = {
+ 'authData.facebook.id': '10000000000000001',
+ };
+ const customOutput = transform.transformWhere('SpamAlerts', customInput, { fields: {} });
+ expect(customOutput['authData.facebook.id']).toBe('10000000000000001');
+ expect(customOutput['_auth_data_facebook.id']).toBeUndefined();
+ });
});
it('cannot have a custom field name beginning with underscore', done => {
diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js
index 6edfa79109..779a97c9f2 100644
--- a/spec/ParseAPI.spec.js
+++ b/spec/ParseAPI.spec.js
@@ -6,7 +6,7 @@ const request = require('../lib/request');
const Parse = require('parse/node');
const Config = require('../lib/Config');
const SchemaController = require('../lib/Controllers/SchemaController');
-const TestUtils = require('../lib/TestUtils');
+const { destroyAllDataPermanently } = require('../lib/TestUtils');
const userSchema = SchemaController.convertSchemaToAdapterSchema({
className: '_User',
@@ -169,7 +169,7 @@ describe('miscellaneous', () => {
}
const config = Config.get('test');
// Remove existing data to clear out unique index
- TestUtils.destroyAllDataPermanently()
+ destroyAllDataPermanently()
.then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] }))
.then(() => config.database.adapter.createClass('_User', userSchema))
.then(() =>
@@ -210,7 +210,7 @@ describe('miscellaneous', () => {
it_id('d00f907e-41b9-40f6-8168-63e832199a8c')(it)('ensure that if people already have duplicate emails, they can still sign up new users', done => {
const config = Config.get('test');
// Remove existing data to clear out unique index
- TestUtils.destroyAllDataPermanently()
+ destroyAllDataPermanently()
.then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] }))
.then(() => config.database.adapter.createClass('_User', userSchema))
.then(() =>
@@ -1710,11 +1710,15 @@ describe('miscellaneous', () => {
});
it('fail on purge all objects in class without master key', done => {
+ const logger = require('../lib/logger').default;
+ const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
+ loggerErrorSpy.calls.reset();
request({
method: 'DELETE',
headers: headers,
@@ -1724,7 +1728,8 @@ describe('miscellaneous', () => {
fail('Should not succeed');
})
.catch(response => {
- expect(response.data.error).toEqual('unauthorized: master key is required');
+ expect(response.data.error).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
done();
});
});
diff --git a/spec/ParseConfigKey.spec.js b/spec/ParseConfigKey.spec.js
index ae31ff954d..2b6881e775 100644
--- a/spec/ParseConfigKey.spec.js
+++ b/spec/ParseConfigKey.spec.js
@@ -73,16 +73,70 @@ describe('Config Keys', () => {
filesAdapter: null,
databaseAdapter: null,
databaseOptions: {
- retryWrites: true,
- maxTimeMS: 1000,
- maxStalenessSeconds: 10,
+ appName: 'MyParseApp',
+
+ // Cannot be tested as it requires authentication setup
+ // authMechanism: 'SCRAM-SHA-256',
+ // authMechanismProperties: { SERVICE_NAME: 'mongodb' },
+
+ authSource: 'admin',
+ autoSelectFamily: true,
+ autoSelectFamilyAttemptTimeout: 3000,
+ compressors: ['zlib'],
+ connectTimeoutMS: 5000,
+ directConnection: false,
+ disableIndexFieldValidation: true,
+ forceServerObjectId: false,
+ heartbeatFrequencyMS: 10000,
+ localThresholdMS: 15,
+ maxConnecting: 2,
+ maxIdleTimeMS: 60000,
maxPoolSize: 10,
+ maxStalenessSeconds: 90,
+ maxTimeMS: 1000,
minPoolSize: 5,
- connectTimeoutMS: 5000,
+
+ // Cannot be tested as it requires a proxy setup
+ // proxyHost: 'proxy.example.com',
+ // proxyPassword: 'proxypass',
+ // proxyPort: 1080,
+ // proxyUsername: 'proxyuser',
+
+ readConcernLevel: 'majority',
+ readPreference: 'secondaryPreferred',
+ readPreferenceTags: [{ dc: 'east' }],
+
+ // Cannot be tested as it requires a replica set setup
+ // replicaSet: 'myReplicaSet',
+
+ retryReads: true,
+ retryWrites: true,
+ serverMonitoringMode: 'auto',
+ serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 5000,
- autoSelectFamily: true,
- autoSelectFamilyAttemptTimeout: 3000,
- disableIndexFieldValidation: true
+
+ // Cannot be tested as it requires a replica cluster setup
+ // srvMaxHosts: 0,
+ // srvServiceName: 'mongodb',
+
+ ssl: false,
+ tls: false,
+ tlsAllowInvalidCertificates: false,
+ tlsAllowInvalidHostnames: false,
+ tlsCAFile: __dirname + '/support/cert/cert.pem',
+ tlsCertificateKeyFile: __dirname + '/support/cert/cert.pem',
+ tlsCertificateKeyFilePassword: 'password',
+ waitQueueTimeoutMS: 5000,
+ zlibCompressionLevel: 6,
+ },
+ })).toBeResolved();
+ await expectAsync(reconfigureServer({
+ databaseURI: 'mongodb://localhost:27017/parse',
+ filesAdapter: null,
+ databaseAdapter: null,
+ databaseOptions: {
+ // The following option needs to be tested separately due to driver config rules
+ tlsInsecure: false,
},
})).toBeResolved();
expect(loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '')).not.toMatch(invalidKeyErrorMessage);
diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js
index d6539b7336..5c1c3c99e7 100644
--- a/spec/ParseFile.spec.js
+++ b/spec/ParseFile.spec.js
@@ -13,6 +13,13 @@ for (let i = 0; i < str.length; i++) {
}
describe('Parse.File testing', () => {
+ let loggerErrorSpy;
+
+ beforeEach(() => {
+ const logger = require('../lib/logger').default;
+ loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+ });
+
describe('creating files', () => {
it('works with Content-Type', done => {
const headers = {
@@ -146,6 +153,7 @@ describe('Parse.File testing', () => {
const b = response.data;
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/);
// missing X-Parse-Master-Key header
+ loggerErrorSpy.calls.reset();
request({
method: 'DELETE',
headers: {
@@ -156,8 +164,10 @@ describe('Parse.File testing', () => {
}).then(fail, response => {
const del_b = response.data;
expect(response.status).toEqual(403);
- expect(del_b.error).toMatch(/unauthorized/);
+ expect(del_b.error).toBe('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
// incorrect X-Parse-Master-Key header
+ loggerErrorSpy.calls.reset();
request({
method: 'DELETE',
headers: {
@@ -169,7 +179,8 @@ describe('Parse.File testing', () => {
}).then(fail, response => {
const del_b2 = response.data;
expect(response.status).toEqual(403);
- expect(del_b2.error).toMatch(/unauthorized/);
+ expect(del_b2.error).toBe('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
done();
});
});
diff --git a/spec/ParseGlobalConfig.spec.js b/spec/ParseGlobalConfig.spec.js
index e6719433ff..1b3a9adc0d 100644
--- a/spec/ParseGlobalConfig.spec.js
+++ b/spec/ParseGlobalConfig.spec.js
@@ -220,6 +220,9 @@ describe('a GlobalConfig', () => {
});
it('fail to update if master key is missing', done => {
+ const logger = require('../lib/logger').default;
+ const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+ loggerErrorSpy.calls.reset();
request({
method: 'PUT',
url: 'http://localhost:8378/1/config',
@@ -233,7 +236,8 @@ describe('a GlobalConfig', () => {
}).then(fail, response => {
const body = response.data;
expect(response.status).toEqual(403);
- expect(body.error).toEqual('unauthorized: master key is required');
+ expect(body.error).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
done();
});
});
diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js
index aee3575079..aa57e973ef 100644
--- a/spec/ParseGraphQLServer.spec.js
+++ b/spec/ParseGraphQLServer.spec.js
@@ -47,6 +47,8 @@ function handleError(e) {
describe('ParseGraphQLServer', () => {
let parseServer;
let parseGraphQLServer;
+ let loggerErrorSpy;
+
beforeEach(async () => {
parseServer = await global.reconfigureServer({
@@ -58,6 +60,9 @@ describe('ParseGraphQLServer', () => {
playgroundPath: '/playground',
subscriptionsPath: '/subscriptions',
});
+
+ const logger = require('../lib/logger').default;
+ loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
});
describe('constructor', () => {
@@ -118,6 +123,20 @@ describe('ParseGraphQLServer', () => {
expect(server3).not.toBe(server2);
expect(server3).toBe(server4);
});
+
+ it('should return same server reference when called 100 times in parallel', async () => {
+ parseGraphQLServer.server = undefined;
+
+ // Call _getServer 100 times in parallel
+ const promises = Array.from({ length: 100 }, () => parseGraphQLServer._getServer());
+ const servers = await Promise.all(promises);
+
+ // All resolved servers should be the same reference
+ const firstServer = servers[0];
+ servers.forEach((server, index) => {
+ expect(server).toBe(firstServer);
+ });
+ });
});
describe('_getGraphQLOptions', () => {
@@ -3474,6 +3493,7 @@ describe('ParseGraphQLServer', () => {
});
it('should require master key to create a new class', async () => {
+ loggerErrorSpy.calls.reset();
try {
await apolloClient.mutate({
mutation: gql`
@@ -3487,7 +3507,8 @@ describe('ParseGraphQLServer', () => {
fail('should fail');
} catch (e) {
expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
- expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required');
+ expect(e.graphQLErrors[0].message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
}
});
@@ -3844,6 +3865,7 @@ describe('ParseGraphQLServer', () => {
handleError(e);
}
+ loggerErrorSpy.calls.reset();
try {
await apolloClient.mutate({
mutation: gql`
@@ -3857,7 +3879,8 @@ describe('ParseGraphQLServer', () => {
fail('should fail');
} catch (e) {
expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
- expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required');
+ expect(e.graphQLErrors[0].message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
}
});
@@ -4069,6 +4092,7 @@ describe('ParseGraphQLServer', () => {
handleError(e);
}
+ loggerErrorSpy.calls.reset();
try {
await apolloClient.mutate({
mutation: gql`
@@ -4082,7 +4106,8 @@ describe('ParseGraphQLServer', () => {
fail('should fail');
} catch (e) {
expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
- expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required');
+ expect(e.graphQLErrors[0].message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
}
});
@@ -4110,6 +4135,7 @@ describe('ParseGraphQLServer', () => {
});
it('should require master key to get an existing class', async () => {
+ loggerErrorSpy.calls.reset();
try {
await apolloClient.query({
query: gql`
@@ -4123,11 +4149,13 @@ describe('ParseGraphQLServer', () => {
fail('should fail');
} catch (e) {
expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
- expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required');
+ expect(e.graphQLErrors[0].message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
}
});
it('should require master key to find the existing classes', async () => {
+ loggerErrorSpy.calls.reset();
try {
await apolloClient.query({
query: gql`
@@ -4141,7 +4169,8 @@ describe('ParseGraphQLServer', () => {
fail('should fail');
} catch (e) {
expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
- expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required');
+ expect(e.graphQLErrors[0].message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
}
});
});
@@ -6067,7 +6096,7 @@ describe('ParseGraphQLServer', () => {
}
await expectAsync(createObject('GraphQLClass')).toBeRejectedWith(
- jasmine.stringMatching('Permission denied for action create on class GraphQLClass')
+ jasmine.stringMatching('Permission denied')
);
await expectAsync(createObject('PublicClass')).toBeResolved();
await expectAsync(
@@ -6101,7 +6130,7 @@ describe('ParseGraphQLServer', () => {
'X-Parse-Session-Token': user4.getSessionToken(),
})
).toBeRejectedWith(
- jasmine.stringMatching('Permission denied for action create on class GraphQLClass')
+ jasmine.stringMatching('Permission denied')
);
await expectAsync(
createObject('PublicClass', {
@@ -7788,7 +7817,8 @@ describe('ParseGraphQLServer', () => {
} catch (err) {
const { graphQLErrors } = err;
expect(graphQLErrors.length).toBe(1);
- expect(graphQLErrors[0].message).toBe('Invalid session token');
+ expect(graphQLErrors[0].message).toBe('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid session token'));
}
});
@@ -7826,7 +7856,8 @@ describe('ParseGraphQLServer', () => {
} catch (err) {
const { graphQLErrors } = err;
expect(graphQLErrors.length).toBe(1);
- expect(graphQLErrors[0].message).toBe('Invalid session token');
+ expect(graphQLErrors[0].message).toBe('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid session token'));
}
});
});
diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js
index c03a727b4a..408e8fa7bf 100644
--- a/spec/ParseInstallation.spec.js
+++ b/spec/ParseInstallation.spec.js
@@ -157,6 +157,9 @@ describe('Installations', () => {
});
it('should properly fail queying installations', done => {
+ const logger = require('../lib/logger').default;
+ const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+
const installId = '12345678-abcd-abcd-abcd-123456789abc';
const device = 'android';
const input = {
@@ -166,6 +169,7 @@ describe('Installations', () => {
rest
.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
+ loggerErrorSpy.calls.reset();
const query = new Parse.Query(Parse.Installation);
return query.find();
})
@@ -174,10 +178,11 @@ describe('Installations', () => {
done();
})
.catch(error => {
- expect(error.code).toBe(119);
+ expect(error.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(error.message).toBe(
- "Clients aren't allowed to perform the find operation on the installation collection."
+ 'Permission denied'
);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the find operation on the installation collection."));
done();
});
});
diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js
index d75658b19e..eb9c03ac4e 100644
--- a/spec/ParseQuery.Aggregate.spec.js
+++ b/spec/ParseQuery.Aggregate.spec.js
@@ -74,10 +74,14 @@ describe('Parse.Query Aggregate testing', () => {
});
it('should only query aggregate with master key', done => {
+ const logger = require('../lib/logger').default;
+ const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+ loggerErrorSpy.calls.reset();
Parse._request('GET', `aggregate/someClass`, {}).then(
() => {},
error => {
- expect(error.message).toEqual('unauthorized: master key is required');
+ expect(error.message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
done();
}
);
diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js
index 98ef70564f..0aa173dc65 100644
--- a/spec/ParseQuery.spec.js
+++ b/spec/ParseQuery.spec.js
@@ -8,6 +8,7 @@ const Parse = require('parse/node');
const request = require('../lib/request');
const ParseServerRESTController = require('../lib/ParseServerRESTController').ParseServerRESTController;
const ParseServer = require('../lib/ParseServer').default;
+const Deprecator = require('../lib/Deprecator/Deprecator').default;
const masterKeyHeaders = {
'X-Parse-Application-Id': 'test',
@@ -4669,6 +4670,91 @@ describe('Parse.Query testing', () => {
.catch(done.fail);
});
+ it('includeAll handles circular pointer references', async () => {
+ // Create two objects that reference each other
+ const objA = new TestObject();
+ const objB = new TestObject();
+
+ objA.set('name', 'Object A');
+ objB.set('name', 'Object B');
+
+ // Save them first
+ await Parse.Object.saveAll([objA, objB]);
+
+ // Create circular references: A -> B -> A
+ objA.set('ref', objB);
+ objB.set('ref', objA);
+
+ await Parse.Object.saveAll([objA, objB]);
+
+ // Query with includeAll
+ const query = new Parse.Query('TestObject');
+ query.equalTo('objectId', objA.id);
+ query.includeAll();
+
+ const results = await query.find();
+
+ // Verify the object is returned
+ expect(results.length).toBe(1);
+ const resultA = results[0];
+ expect(resultA.get('name')).toBe('Object A');
+
+ // Verify the immediate reference is included (1 level deep)
+ const refB = resultA.get('ref');
+ expect(refB).toBeDefined();
+ expect(refB.get('name')).toBe('Object B');
+
+ // Verify that includeAll only includes 1 level deep
+ // B's pointer back to A should exist as an object but without full data
+ const refBackToA = refB.get('ref');
+ expect(refBackToA).toBeDefined();
+ expect(refBackToA.id).toBe(objA.id);
+
+ // The circular reference exists but is NOT fully populated
+ // (name field is undefined because it's not included at this depth)
+ expect(refBackToA.get('name')).toBeUndefined();
+
+ // Verify using toJSON that it's stored as a pointer
+ const refBackToAJSON = refB.toJSON().ref;
+ expect(refBackToAJSON).toBeDefined();
+ expect(refBackToAJSON.__type).toBe('Pointer');
+ expect(refBackToAJSON.className).toBe('TestObject');
+ expect(refBackToAJSON.objectId).toBe(objA.id);
+ });
+
+ it('includeAll handles self-referencing pointer', async () => {
+ // Create an object that points to itself
+ const selfRef = new TestObject();
+ selfRef.set('name', 'Self-Referencing');
+
+ await selfRef.save();
+
+ // Make it point to itself
+ selfRef.set('ref', selfRef);
+ await selfRef.save();
+
+ // Query with includeAll
+ const query = new Parse.Query('TestObject');
+ query.equalTo('objectId', selfRef.id);
+ query.includeAll();
+
+ const results = await query.find();
+
+ // Verify the object is returned
+ expect(results.length).toBe(1);
+ const result = results[0];
+ expect(result.get('name')).toBe('Self-Referencing');
+
+ // Verify the self-reference is included (since it's at the first level)
+ const ref = result.get('ref');
+ expect(ref).toBeDefined();
+ expect(ref.id).toBe(selfRef.id);
+
+ // The self-reference should be fully populated at the first level
+ // because includeAll includes all pointer fields at the immediate level
+ expect(ref.get('name')).toBe('Self-Referencing');
+ });
+
it('select nested keys 2 level without include (issue #3185)', function (done) {
const Foobar = new Parse.Object('Foobar');
const BarBaz = new Parse.Object('Barbaz');
@@ -5384,4 +5470,102 @@ describe('Parse.Query testing', () => {
expect(query1.length).toEqual(1);
});
});
+
+ describe('allowPublicExplain', () => {
+ it_id('a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d')(it_only_db('mongo'))(
+ 'explain works with and without master key when allowPublicExplain is true',
+ async () => {
+ await reconfigureServer({
+ databaseAdapter: undefined,
+ databaseURI: 'mongodb://localhost:27017/parse',
+ databaseOptions: {
+ allowPublicExplain: true,
+ },
+ });
+
+ const obj = new TestObject({ foo: 'bar' });
+ await obj.save();
+
+ // Without master key
+ const query = new Parse.Query(TestObject);
+ query.explain();
+ const resultWithoutMasterKey = await query.find();
+ expect(resultWithoutMasterKey).toBeDefined();
+
+ // With master key
+ const queryWithMasterKey = new Parse.Query(TestObject);
+ queryWithMasterKey.explain();
+ const resultWithMasterKey = await queryWithMasterKey.find({ useMasterKey: true });
+ expect(resultWithMasterKey).toBeDefined();
+ }
+ );
+
+ it_id('b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e')(it_only_db('mongo'))(
+ 'explain requires master key when allowPublicExplain is false',
+ async () => {
+ await reconfigureServer({
+ databaseAdapter: undefined,
+ databaseURI: 'mongodb://localhost:27017/parse',
+ databaseOptions: {
+ allowPublicExplain: false,
+ },
+ });
+
+ const obj = new TestObject({ foo: 'bar' });
+ await obj.save();
+
+ // Without master key
+ const query = new Parse.Query(TestObject);
+ query.explain();
+ await expectAsync(query.find()).toBeRejectedWith(
+ new Parse.Error(
+ Parse.Error.INVALID_QUERY,
+ 'Using the explain query parameter requires the master key'
+ )
+ );
+
+ // With master key
+ const queryWithMasterKey = new Parse.Query(TestObject);
+ queryWithMasterKey.explain();
+ const result = await queryWithMasterKey.find({ useMasterKey: true });
+ expect(result).toBeDefined();
+ }
+ );
+
+ it_id('c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f')(it_only_db('mongo'))(
+ 'explain works with and without master key by default',
+ async () => {
+ const logger = require('../lib/logger').logger;
+ const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
+
+ await reconfigureServer({
+ databaseAdapter: undefined,
+ databaseURI: 'mongodb://localhost:27017/parse',
+ databaseOptions: {
+ allowPublicExplain: undefined,
+ },
+ });
+
+ // Verify deprecation warning is logged when allowPublicExplain is not explicitly set
+ expect(logSpy).toHaveBeenCalledWith(
+ jasmine.stringMatching(/DeprecationWarning.*databaseOptions\.allowPublicExplain.*false/)
+ );
+
+ const obj = new TestObject({ foo: 'bar' });
+ await obj.save();
+
+ // Without master key
+ const query = new Parse.Query(TestObject);
+ query.explain();
+ const resultWithoutMasterKey = await query.find();
+ expect(resultWithoutMasterKey).toBeDefined();
+
+ // With master key
+ const queryWithMasterKey = new Parse.Query(TestObject);
+ queryWithMasterKey.explain();
+ const resultWithMasterKey = await queryWithMasterKey.find({ useMasterKey: true });
+ expect(resultWithMasterKey).toBeDefined();
+ }
+ );
+ });
});
diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js
index ba34fbf6e9..0380589057 100644
--- a/spec/ParseUser.spec.js
+++ b/spec/ParseUser.spec.js
@@ -13,6 +13,7 @@ const passwordCrypto = require('../lib/password');
const Config = require('../lib/Config');
const cryptoUtils = require('../lib/cryptoUtils');
+
describe('allowExpiredAuthDataToken option', () => {
it('should accept true value', async () => {
await reconfigureServer({ allowExpiredAuthDataToken: true });
@@ -38,6 +39,12 @@ describe('allowExpiredAuthDataToken option', () => {
});
describe('Parse.User testing', () => {
+ let loggerErrorSpy;
+ beforeEach(() => {
+ const logger = require('../lib/logger').default;
+ loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+ });
+
it('user sign up class method', async done => {
const user = await Parse.User.signUp('asdf', 'zxcv');
ok(user.getSessionToken());
@@ -2651,6 +2658,7 @@ describe('Parse.User testing', () => {
const b = response.data;
expect(b.results.length).toEqual(1);
const objId = b.results[0].objectId;
+ loggerErrorSpy.calls.reset();
request({
method: 'DELETE',
headers: {
@@ -2661,7 +2669,9 @@ describe('Parse.User testing', () => {
}).then(fail, response => {
const b = response.data;
expect(b.code).toEqual(209);
- expect(b.error).toBe('Invalid session token');
+ expect(b.error).toBe('Permission denied');
+
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid session token'));
done();
});
});
@@ -3355,6 +3365,9 @@ describe('Parse.User testing', () => {
sendMail: () => Promise.resolve(),
};
+ let logger;
+ let loggerErrorSpy;
+
const user = new Parse.User();
user.set({
username: 'hello',
@@ -3369,9 +3382,12 @@ describe('Parse.User testing', () => {
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
+ logger = require('../lib/logger').default;
+ loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
return user.signUp();
})
.then(() => {
+ loggerErrorSpy.calls.reset();
return Parse.User.current().set('emailVerified', true).save();
})
.then(() => {
@@ -3379,7 +3395,9 @@ describe('Parse.User testing', () => {
done();
})
.catch(err => {
- expect(err.message).toBe("Clients aren't allowed to manually update email verification.");
+ expect(err.message).toBe('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to manually update email verification."));
+
done();
});
});
@@ -4277,6 +4295,12 @@ describe('Security Advisory GHSA-8w3j-g983-8jh5', function () {
});
describe('login as other user', () => {
+ let loggerErrorSpy;
+ beforeEach(() => {
+ const logger = require('../lib/logger').default;
+ loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+ });
+
it('allows creating a session for another user with the master key', async done => {
await Parse.User.signUp('some_user', 'some_password');
const userId = Parse.User.current().id;
@@ -4376,6 +4400,7 @@ describe('login as other user', () => {
const userId = Parse.User.current().id;
await Parse.User.logOut();
+ loggerErrorSpy.calls.reset();
try {
await request({
method: 'POST',
@@ -4393,7 +4418,8 @@ describe('login as other user', () => {
done();
} catch (err) {
expect(err.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
- expect(err.data.error).toBe('master key is required');
+ expect(err.data.error).toBe('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('master key is required'));
}
const sessionsQuery = new Parse.Query(Parse.Session);
diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js
index 6fe3c0fa18..fb5370d759 100644
--- a/spec/RestQuery.spec.js
+++ b/spec/RestQuery.spec.js
@@ -5,7 +5,6 @@ const Config = require('../lib/Config');
const rest = require('../lib/rest');
const RestQuery = require('../lib/RestQuery');
const request = require('../lib/request');
-
const querystring = require('querystring');
let config;
@@ -155,9 +154,13 @@ describe('rest query', () => {
});
it('query non-existent class when disabled client class creation', done => {
+ const logger = require('../lib/logger').default;
+ const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+
const customConfig = Object.assign({}, config, {
allowClientClassCreation: false,
});
+ loggerErrorSpy.calls.reset();
rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}).then(
() => {
fail('Should throw an error');
@@ -165,9 +168,8 @@ describe('rest query', () => {
},
err => {
expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
- expect(err.message).toEqual(
- 'This user is not allowed to access ' + 'non-existent class: ClientClassCreation'
- );
+ expect(err.message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('This user is not allowed to access ' + 'non-existent class: ClientClassCreation'));
done();
}
);
@@ -243,7 +245,7 @@ describe('rest query', () => {
expectAsync(new Parse.Query('Test').exists('zip').find()).toBeRejectedWith(
new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
- 'This user is not allowed to query zip on class Test'
+ 'Permission denied'
)
),
]);
@@ -386,6 +388,88 @@ describe('rest query', () => {
}
);
});
+
+ it('battle test parallel include with 100 nested includes', async () => {
+ const RootObject = Parse.Object.extend('RootObject');
+ const Level1Object = Parse.Object.extend('Level1Object');
+ const Level2Object = Parse.Object.extend('Level2Object');
+
+ // Create 100 level2 objects (10 per level1 object)
+ const level2Objects = [];
+ for (let i = 0; i < 100; i++) {
+ const level2 = new Level2Object({
+ index: i,
+ value: `level2_${i}`,
+ });
+ level2Objects.push(level2);
+ }
+ await Parse.Object.saveAll(level2Objects);
+
+ // Create 10 level1 objects, each with 10 pointers to level2 objects
+ const level1Objects = [];
+ for (let i = 0; i < 10; i++) {
+ const level1 = new Level1Object({
+ index: i,
+ value: `level1_${i}`,
+ });
+ // Set 10 pointer fields (level2_0 through level2_9)
+ for (let j = 0; j < 10; j++) {
+ level1.set(`level2_${j}`, level2Objects[i * 10 + j]);
+ }
+ level1Objects.push(level1);
+ }
+ await Parse.Object.saveAll(level1Objects);
+
+ // Create 1 root object with 10 pointers to level1 objects
+ const rootObject = new RootObject({
+ value: 'root',
+ });
+ for (let i = 0; i < 10; i++) {
+ rootObject.set(`level1_${i}`, level1Objects[i]);
+ }
+ await rootObject.save();
+
+ // Build include paths: level1_0 through level1_9, and level1_0.level2_0 through level1_9.level2_9
+ const includePaths = [];
+ for (let i = 0; i < 10; i++) {
+ includePaths.push(`level1_${i}`);
+ for (let j = 0; j < 10; j++) {
+ includePaths.push(`level1_${i}.level2_${j}`);
+ }
+ }
+
+ // Query with all includes
+ const query = new Parse.Query(RootObject);
+ query.equalTo('objectId', rootObject.id);
+ for (const path of includePaths) {
+ query.include(path);
+ }
+ console.time('query.find');
+ const results = await query.find();
+ console.timeEnd('query.find');
+ expect(results.length).toBe(1);
+
+ const result = results[0];
+ expect(result.id).toBe(rootObject.id);
+
+ // Verify all 10 level1 objects are included
+ for (let i = 0; i < 10; i++) {
+ const level1Field = result.get(`level1_${i}`);
+ expect(level1Field).toBeDefined();
+ expect(level1Field instanceof Parse.Object).toBe(true);
+ expect(level1Field.get('index')).toBe(i);
+ expect(level1Field.get('value')).toBe(`level1_${i}`);
+
+ // Verify all 10 level2 objects are included for each level1 object
+ for (let j = 0; j < 10; j++) {
+ const level2Field = level1Field.get(`level2_${j}`);
+ expect(level2Field).toBeDefined();
+ expect(level2Field instanceof Parse.Object).toBe(true);
+ expect(level2Field.get('index')).toBe(i * 10 + j);
+ expect(level2Field.get('value')).toBe(`level2_${i * 10 + j}`);
+ }
+ }
+ });
});
describe('RestQuery.each', () => {
diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js
index 2192678797..03c68276f8 100644
--- a/spec/Schema.spec.js
+++ b/spec/Schema.spec.js
@@ -20,8 +20,12 @@ const hasAllPODobject = () => {
};
describe('SchemaController', () => {
+ let loggerErrorSpy;
+
beforeEach(() => {
config = Config.get('test');
+ const logger = require('../lib/logger').default;
+ loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
});
it('can validate one object', done => {
@@ -275,6 +279,7 @@ describe('SchemaController', () => {
})
.then(results => {
expect(results.length).toBe(1);
+ loggerErrorSpy.calls.reset();
const query = new Parse.Query('Stuff');
return query.count();
})
@@ -283,7 +288,9 @@ describe('SchemaController', () => {
fail('Class permissions should have rejected this query.');
},
err => {
- expect(err.message).toEqual('Permission denied for action count on class Stuff.');
+ expect(err.message).toEqual('Permission denied');
+ expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action count on class Stuff'));
done();
}
)
@@ -1427,8 +1434,12 @@ describe('SchemaController', () => {
});
describe('Class Level Permissions for requiredAuth', () => {
+ let loggerErrorSpy;
+
beforeEach(() => {
config = Config.get('test');
+ const logger = require('../lib/logger').default;
+ loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
});
function createUser() {
@@ -1453,6 +1464,7 @@ describe('Class Level Permissions for requiredAuth', () => {
});
})
.then(() => {
+ loggerErrorSpy.calls.reset();
const query = new Parse.Query('Stuff');
return query.find();
})
@@ -1462,7 +1474,8 @@ describe('Class Level Permissions for requiredAuth', () => {
done();
},
e => {
- expect(e.message).toEqual('Permission denied, user needs to be authenticated.');
+ expect(e.message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.'));
done();
}
);
@@ -1551,6 +1564,7 @@ describe('Class Level Permissions for requiredAuth', () => {
});
})
.then(() => {
+ loggerErrorSpy.calls.reset();
const stuff = new Parse.Object('Stuff');
stuff.set('foo', 'bar');
return stuff.save();
@@ -1561,7 +1575,8 @@ describe('Class Level Permissions for requiredAuth', () => {
done();
},
e => {
- expect(e.message).toEqual('Permission denied, user needs to be authenticated.');
+ expect(e.message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.'));
done();
}
);
@@ -1639,6 +1654,7 @@ describe('Class Level Permissions for requiredAuth', () => {
const stuff = new Parse.Object('Stuff');
stuff.set('foo', 'bar');
return stuff.save().then(() => {
+ loggerErrorSpy.calls.reset();
const query = new Parse.Query('Stuff');
return query.get(stuff.id);
});
@@ -1649,7 +1665,8 @@ describe('Class Level Permissions for requiredAuth', () => {
done();
},
e => {
- expect(e.message).toEqual('Permission denied, user needs to be authenticated.');
+ expect(e.message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.'));
done();
}
);
@@ -1685,6 +1702,7 @@ describe('Class Level Permissions for requiredAuth', () => {
})
.then(result => {
expect(result.get('foo')).toEqual('bar');
+ loggerErrorSpy.calls.reset();
const query = new Parse.Query('Stuff');
return query.find();
})
@@ -1694,7 +1712,8 @@ describe('Class Level Permissions for requiredAuth', () => {
done();
},
e => {
- expect(e.message).toEqual('Permission denied, user needs to be authenticated.');
+ expect(e.message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.'));
done();
}
);
diff --git a/spec/SecurityCheckGroups.spec.js b/spec/SecurityCheckGroups.spec.js
index 3e5f312dd7..aea4468da8 100644
--- a/spec/SecurityCheckGroups.spec.js
+++ b/spec/SecurityCheckGroups.spec.js
@@ -60,6 +60,26 @@ describe('Security Check Groups', () => {
expect(group.checks()[4].checkState()).toBe(CheckState.fail);
expect(group.checks()[5].checkState()).toBe(CheckState.fail);
});
+
+ it_only_db('mongo')('checks succeed correctly (MongoDB specific)', async () => {
+ config.databaseAdapter = undefined;
+ config.databaseOptions = { allowPublicExplain: false };
+ await reconfigureServer(config);
+
+ const group = new CheckGroupServerConfig();
+ await group.run();
+ expect(group.checks()[6].checkState()).toBe(CheckState.success);
+ });
+
+ it_only_db('mongo')('checks fail correctly (MongoDB specific)', async () => {
+ config.databaseAdapter = undefined;
+ config.databaseOptions = { allowPublicExplain: true };
+ await reconfigureServer(config);
+
+ const group = new CheckGroupServerConfig();
+ await group.run();
+ expect(group.checks()[6].checkState()).toBe(CheckState.fail);
+ });
});
describe('CheckGroupDatabase', () => {
diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js
index 14747af6aa..a473064376 100644
--- a/spec/Utils.spec.js
+++ b/spec/Utils.spec.js
@@ -1,4 +1,5 @@
-const Utils = require('../src/Utils');
+const Utils = require('../lib/Utils');
+const { createSanitizedError, createSanitizedHttpError } = require("../lib/Error")
describe('Utils', () => {
describe('encodeForUrl', () => {
@@ -57,4 +58,158 @@ describe('Utils', () => {
});
});
});
+
+ describe('getCircularReplacer', () => {
+ it('should handle Map instances', () => {
+ const obj = {
+ name: 'test',
+ mapData: new Map([
+ ['key1', 'value1'],
+ ['key2', 'value2']
+ ])
+ };
+ const result = JSON.stringify(obj, Utils.getCircularReplacer());
+ expect(result).toBe('{"name":"test","mapData":{"key1":"value1","key2":"value2"}}');
+ });
+
+ it('should handle Set instances', () => {
+ const obj = {
+ name: 'test',
+ setData: new Set([1, 2, 3])
+ };
+ const result = JSON.stringify(obj, Utils.getCircularReplacer());
+ expect(result).toBe('{"name":"test","setData":[1,2,3]}');
+ });
+
+ it('should handle circular references', () => {
+ const obj = { name: 'test', value: 123 };
+ obj.self = obj;
+ const result = JSON.stringify(obj, Utils.getCircularReplacer());
+ expect(result).toBe('{"name":"test","value":123,"self":"[Circular]"}');
+ });
+
+ it('should handle nested circular references', () => {
+ const obj = {
+ name: 'parent',
+ child: {
+ name: 'child'
+ }
+ };
+ obj.child.parent = obj;
+ const result = JSON.stringify(obj, Utils.getCircularReplacer());
+ expect(result).toBe('{"name":"parent","child":{"name":"child","parent":"[Circular]"}}');
+ });
+
+ it('should handle mixed Map, Set, and circular references', () => {
+ const obj = {
+ mapData: new Map([['key', 'value']]),
+ setData: new Set([1, 2]),
+ regular: 'data'
+ };
+ obj.circular = obj;
+ const result = JSON.stringify(obj, Utils.getCircularReplacer());
+ expect(result).toBe('{"mapData":{"key":"value"},"setData":[1,2],"regular":"data","circular":"[Circular]"}');
+ });
+
+ it('should handle normal objects without modification', () => {
+ const obj = {
+ name: 'test',
+ number: 42,
+ nested: {
+ key: 'value'
+ }
+ };
+ const result = JSON.stringify(obj, Utils.getCircularReplacer());
+ expect(result).toBe('{"name":"test","number":42,"nested":{"key":"value"}}');
+ });
+ });
+
+ describe('getNestedProperty', () => {
+ it('should get top-level property', () => {
+ const obj = { foo: 'bar' };
+ expect(Utils.getNestedProperty(obj, 'foo')).toBe('bar');
+ });
+
+ it('should get nested property with dot notation', () => {
+ const obj = { database: { options: { enabled: true } } };
+ expect(Utils.getNestedProperty(obj, 'database.options.enabled')).toBe(true);
+ });
+
+ it('should return undefined for non-existent property', () => {
+ const obj = { foo: 'bar' };
+ expect(Utils.getNestedProperty(obj, 'baz')).toBeUndefined();
+ });
+
+ it('should return undefined for non-existent nested property', () => {
+ const obj = { database: { options: {} } };
+ expect(Utils.getNestedProperty(obj, 'database.options.enabled')).toBeUndefined();
+ });
+
+ it('should return undefined when path traverses non-object', () => {
+ const obj = { database: 'string' };
+ expect(Utils.getNestedProperty(obj, 'database.options.enabled')).toBeUndefined();
+ });
+
+ it('should return undefined for null object', () => {
+ expect(Utils.getNestedProperty(null, 'foo')).toBeUndefined();
+ });
+
+ it('should return undefined for empty path', () => {
+ const obj = { foo: 'bar' };
+ expect(Utils.getNestedProperty(obj, '')).toBeUndefined();
+ });
+
+ it('should handle value of 0', () => {
+ const obj = { database: { timeout: 0 } };
+ expect(Utils.getNestedProperty(obj, 'database.timeout')).toBe(0);
+ });
+
+ it('should handle value of false', () => {
+ const obj = { database: { enabled: false } };
+ expect(Utils.getNestedProperty(obj, 'database.enabled')).toBe(false);
+ });
+
+ it('should handle value of empty string', () => {
+ const obj = { database: { name: '' } };
+ expect(Utils.getNestedProperty(obj, 'database.name')).toBe('');
+ });
+ });
+
+ describe('createSanitizedError', () => {
+ it('should return "Permission denied" when enableSanitizedErrorResponse is true', () => {
+ const config = { enableSanitizedErrorResponse: true };
+ const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config);
+ expect(error.message).toBe('Permission denied');
+ });
+
+ it('should not crash with config undefined', () => {
+ const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', undefined);
+ expect(error.message).toBe('Permission denied');
+ });
+
+ it('should return the detailed message when enableSanitizedErrorResponse is false', () => {
+ const config = { enableSanitizedErrorResponse: false };
+ const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config);
+ expect(error.message).toBe('Detailed error message');
+ });
+ });
+
+ describe('createSanitizedHttpError', () => {
+ it('should return "Permission denied" when enableSanitizedErrorResponse is true', () => {
+ const config = { enableSanitizedErrorResponse: true };
+ const error = createSanitizedHttpError(403, 'Detailed error message', config);
+ expect(error.message).toBe('Permission denied');
+ });
+
+ it('should not crash with config undefined', () => {
+ const error = createSanitizedHttpError(403, 'Detailed error message', undefined);
+ expect(error.message).toBe('Permission denied');
+ });
+
+ it('should return the detailed message when enableSanitizedErrorResponse is false', () => {
+ const config = { enableSanitizedErrorResponse: false };
+ const error = createSanitizedHttpError(403, 'Detailed error message', config);
+ expect(error.message).toBe('Detailed error message');
+ });
+ });
});
diff --git a/spec/features.spec.js b/spec/features.spec.js
index f138fe4cf6..201e01293d 100644
--- a/spec/features.spec.js
+++ b/spec/features.spec.js
@@ -20,6 +20,9 @@ describe('features', () => {
});
it('requires the master key to get features', async done => {
+ const logger = require('../lib/logger').default;
+ const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+ loggerErrorSpy.calls.reset();
try {
await request({
url: 'http://localhost:8378/1/serverInfo',
@@ -32,7 +35,8 @@ describe('features', () => {
done.fail('The serverInfo request should be rejected without the master key');
} catch (error) {
expect(error.status).toEqual(403);
- expect(error.data.error).toEqual('unauthorized: master key is required');
+ expect(error.data.error).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
done();
}
});
diff --git a/spec/index.spec.js b/spec/index.spec.js
index 5093a6ea25..afc1b5362e 100644
--- a/spec/index.spec.js
+++ b/spec/index.spec.js
@@ -363,7 +363,7 @@ describe('server', () => {
it('should throw when getting invalid mount', done => {
reconfigureServer({ publicServerURL: 'blabla:/some' }).catch(error => {
- expect(error).toEqual('publicServerURL should be a valid HTTPS URL starting with https://');
+ expect(error).toEqual('The option publicServerURL must be a valid URL starting with http:// or https://.');
done();
});
});
@@ -685,4 +685,171 @@ describe('server', () => {
})
.catch(done.fail);
});
+
+ describe('publicServerURL', () => {
+ it('should load publicServerURL', async () => {
+ await reconfigureServer({
+ publicServerURL: () => 'https://example.com/1',
+ });
+
+ await new Parse.Object('TestObject').save();
+
+ const config = Config.get(Parse.applicationId);
+ expect(config.publicServerURL).toEqual('https://example.com/1');
+ });
+
+ it('should load publicServerURL from Promise', async () => {
+ await reconfigureServer({
+ publicServerURL: () => Promise.resolve('https://example.com/1'),
+ });
+
+ await new Parse.Object('TestObject').save();
+
+ const config = Config.get(Parse.applicationId);
+ expect(config.publicServerURL).toEqual('https://example.com/1');
+ });
+
+ it('should handle publicServerURL function throwing error', async () => {
+ const errorMessage = 'Failed to get public server URL';
+ await reconfigureServer({
+ publicServerURL: () => {
+ throw new Error(errorMessage);
+ },
+ });
+
+ // The error should occur when trying to save an object (which triggers loadKeys in middleware)
+ await expectAsync(
+ new Parse.Object('TestObject').save()
+ ).toBeRejected();
+ });
+
+ it('should handle publicServerURL Promise rejection', async () => {
+ const errorMessage = 'Async fetch of public server URL failed';
+ await reconfigureServer({
+ publicServerURL: () => Promise.reject(new Error(errorMessage)),
+ });
+
+ // The error should occur when trying to save an object (which triggers loadKeys in middleware)
+ await expectAsync(
+ new Parse.Object('TestObject').save()
+ ).toBeRejected();
+ });
+
+ it('executes publicServerURL function on every config access', async () => {
+ let counter = 0;
+ await reconfigureServer({
+ publicServerURL: () => {
+ counter++;
+ return `https://example.com/${counter}`;
+ },
+ });
+
+ // First request - should call the function
+ await new Parse.Object('TestObject').save();
+ const config1 = Config.get(Parse.applicationId);
+ expect(config1.publicServerURL).toEqual('https://example.com/1');
+ expect(counter).toEqual(1);
+
+ // Second request - should call the function again
+ await new Parse.Object('TestObject').save();
+ const config2 = Config.get(Parse.applicationId);
+ expect(config2.publicServerURL).toEqual('https://example.com/2');
+ expect(counter).toEqual(2);
+
+ // Third request - should call the function again
+ await new Parse.Object('TestObject').save();
+ const config3 = Config.get(Parse.applicationId);
+ expect(config3.publicServerURL).toEqual('https://example.com/3');
+ expect(counter).toEqual(3);
+ });
+
+ it('executes publicServerURL function on every password reset email', async () => {
+ let counter = 0;
+ const emailCalls = [];
+
+ const emailAdapter = MockEmailAdapterWithOptions({
+ sendPasswordResetEmail: ({ link }) => {
+ emailCalls.push(link);
+ return Promise.resolve();
+ },
+ });
+
+ await reconfigureServer({
+ appName: 'test-app',
+ publicServerURL: () => {
+ counter++;
+ return `https://example.com/${counter}`;
+ },
+ emailAdapter,
+ });
+
+ // Create a user
+ const user = new Parse.User();
+ user.setUsername('user');
+ user.setPassword('pass');
+ user.setEmail('user@example.com');
+ await user.signUp();
+
+ // Should use first publicServerURL
+ const counterBefore1 = counter;
+ await Parse.User.requestPasswordReset('user@example.com');
+ await jasmine.timeout();
+ expect(emailCalls.length).toEqual(1);
+ expect(emailCalls[0]).toContain(`https://example.com/${counterBefore1 + 1}`);
+ expect(counter).toBeGreaterThanOrEqual(2);
+
+ // Should use updated publicServerURL
+ const counterBefore2 = counter;
+ await Parse.User.requestPasswordReset('user@example.com');
+ await jasmine.timeout();
+ expect(emailCalls.length).toEqual(2);
+ expect(emailCalls[1]).toContain(`https://example.com/${counterBefore2 + 1}`);
+ expect(counterBefore2).toBeGreaterThan(counterBefore1);
+ });
+
+ it('executes publicServerURL function on every verification email', async () => {
+ let counter = 0;
+ const emailCalls = [];
+
+ const emailAdapter = MockEmailAdapterWithOptions({
+ sendVerificationEmail: ({ link }) => {
+ emailCalls.push(link);
+ return Promise.resolve();
+ },
+ });
+
+ await reconfigureServer({
+ appName: 'test-app',
+ verifyUserEmails: true,
+ publicServerURL: () => {
+ counter++;
+ return `https://example.com/${counter}`;
+ },
+ emailAdapter,
+ });
+
+ // Should trigger verification email with first publicServerURL
+ const counterBefore1 = counter;
+ const user1 = new Parse.User();
+ user1.setUsername('user1');
+ user1.setPassword('pass1');
+ user1.setEmail('user1@example.com');
+ await user1.signUp();
+ await jasmine.timeout();
+ expect(emailCalls.length).toEqual(1);
+ expect(emailCalls[0]).toContain(`https://example.com/${counterBefore1 + 1}`);
+
+ // Should trigger verification email with updated publicServerURL
+ const counterBefore2 = counter;
+ const user2 = new Parse.User();
+ user2.setUsername('user2');
+ user2.setPassword('pass2');
+ user2.setEmail('user2@example.com');
+ await user2.signUp();
+ await jasmine.timeout();
+ expect(emailCalls.length).toEqual(2);
+ expect(emailCalls[1]).toContain(`https://example.com/${counterBefore2 + 1}`);
+ expect(counterBefore2).toBeGreaterThan(counterBefore1);
+ });
+ });
});
diff --git a/spec/rest.spec.js b/spec/rest.spec.js
index 1fff4fad59..4d8f40a982 100644
--- a/spec/rest.spec.js
+++ b/spec/rest.spec.js
@@ -11,9 +11,14 @@ let config;
let database;
describe('rest create', () => {
+ let loggerErrorSpy;
+
beforeEach(() => {
config = Config.get('test');
database = config.database;
+
+ const logger = require('../lib/logger').default;
+ loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
});
it('handles _id', done => {
@@ -317,6 +322,7 @@ describe('rest create', () => {
const customConfig = Object.assign({}, config, {
allowClientClassCreation: false,
});
+ loggerErrorSpy.calls.reset();
rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}).then(
() => {
fail('Should throw an error');
@@ -324,9 +330,8 @@ describe('rest create', () => {
},
err => {
expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
- expect(err.message).toEqual(
- 'This user is not allowed to access ' + 'non-existent class: ClientClassCreation'
- );
+ expect(err.message).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('This user is not allowed to access ' + 'non-existent class: ClientClassCreation'));
done();
}
);
@@ -772,6 +777,7 @@ describe('rest create', () => {
});
it('cannot get object in volatileClasses if not masterKey through pointer', async () => {
+ loggerErrorSpy.calls.reset();
const masterKeyOnlyClassObject = new Parse.Object('_PushStatus');
await masterKeyOnlyClassObject.save(null, { useMasterKey: true });
const obj2 = new Parse.Object('TestObject');
@@ -783,11 +789,13 @@ describe('rest create', () => {
const query = new Parse.Query('TestObject');
query.include('pointer');
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
- "Clients aren't allowed to perform the get operation on the _PushStatus collection."
+ 'Permission denied'
);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the get operation on the _PushStatus collection."));
});
it_id('3ce563bf-93aa-4d0b-9af9-c5fb246ac9fc')(it)('cannot get object in _GlobalConfig if not masterKey through pointer', async () => {
+ loggerErrorSpy.calls.reset();
await Parse.Config.save({ privateData: 'secret' }, { privateData: true });
const obj2 = new Parse.Object('TestObject');
obj2.set('globalConfigPointer', {
@@ -799,8 +807,9 @@ describe('rest create', () => {
const query = new Parse.Query('TestObject');
query.include('globalConfigPointer');
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
- "Clients aren't allowed to perform the get operation on the _GlobalConfig collection."
+ 'Permission denied'
);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the get operation on the _GlobalConfig collection."));
});
it('locks down session', done => {
@@ -945,7 +954,16 @@ describe('rest update', () => {
});
describe('read-only masterKey', () => {
+ let loggerErrorSpy;
+ let logger;
+
+ beforeEach(() => {
+ logger = require('../lib/logger').default;
+ loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+ });
+
it('properly throws on rest.create, rest.update and rest.del', () => {
+ loggerErrorSpy.calls.reset();
const config = Config.get('test');
const readOnly = auth.readOnly(config);
expect(() => {
@@ -953,9 +971,10 @@ describe('read-only masterKey', () => {
}).toThrow(
new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
- `read-only masterKey isn't allowed to perform the create operation.`
+ 'Permission denied'
)
);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to perform the create operation."));
expect(() => {
rest.update(config, readOnly, 'AnObject', {});
}).toThrow();
@@ -968,6 +987,9 @@ describe('read-only masterKey', () => {
await reconfigureServer({
readOnlyMasterKey: 'yolo-read-only',
});
+ // Need to be re required because reconfigureServer resets the logger
+ const logger2 = require('../lib/logger').default;
+ loggerErrorSpy = spyOn(logger2, 'error').and.callThrough();
try {
await request({
url: `${Parse.serverURL}/classes/MyYolo`,
@@ -983,8 +1005,9 @@ describe('read-only masterKey', () => {
} catch (res) {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe(
- "read-only masterKey isn't allowed to perform the create operation."
+ 'Permission denied'
);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to perform the create operation."));
}
await reconfigureServer();
});
@@ -1012,18 +1035,18 @@ describe('read-only masterKey', () => {
});
it('should throw when trying to create RestWrite', () => {
+ loggerErrorSpy.calls.reset();
const config = Config.get('test');
expect(() => {
new RestWrite(config, auth.readOnly(config));
}).toThrow(
- new Parse.Error(
- Parse.Error.OPERATION_FORBIDDEN,
- 'Cannot perform a write operation when using readOnlyMasterKey'
- )
+ new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')
);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Cannot perform a write operation when using readOnlyMasterKey"));
});
it('should throw when trying to create schema', done => {
+ loggerErrorSpy.calls.reset();
request({
method: 'POST',
url: `${Parse.serverURL}/schemas`,
@@ -1037,12 +1060,14 @@ describe('read-only masterKey', () => {
.then(done.fail)
.catch(res => {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
- expect(res.data.error).toBe("read-only masterKey isn't allowed to create a schema.");
+ expect(res.data.error).toBe('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to create a schema."));
done();
});
});
it('should throw when trying to create schema with a name', done => {
+ loggerErrorSpy.calls.reset();
request({
url: `${Parse.serverURL}/schemas/MyClass`,
method: 'POST',
@@ -1056,12 +1081,14 @@ describe('read-only masterKey', () => {
.then(done.fail)
.catch(res => {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
- expect(res.data.error).toBe("read-only masterKey isn't allowed to create a schema.");
+ expect(res.data.error).toBe('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to create a schema."));
done();
});
});
it('should throw when trying to update schema', done => {
+ loggerErrorSpy.calls.reset();
request({
url: `${Parse.serverURL}/schemas/MyClass`,
method: 'PUT',
@@ -1075,12 +1102,14 @@ describe('read-only masterKey', () => {
.then(done.fail)
.catch(res => {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
- expect(res.data.error).toBe("read-only masterKey isn't allowed to update a schema.");
+ expect(res.data.error).toBe('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to update a schema."));
done();
});
});
it('should throw when trying to delete schema', done => {
+ loggerErrorSpy.calls.reset();
request({
url: `${Parse.serverURL}/schemas/MyClass`,
method: 'DELETE',
@@ -1094,12 +1123,14 @@ describe('read-only masterKey', () => {
.then(done.fail)
.catch(res => {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
- expect(res.data.error).toBe("read-only masterKey isn't allowed to delete a schema.");
+ expect(res.data.error).toBe('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to delete a schema."));
done();
});
});
it('should throw when trying to update the global config', done => {
+ loggerErrorSpy.calls.reset();
request({
url: `${Parse.serverURL}/config`,
method: 'PUT',
@@ -1113,12 +1144,14 @@ describe('read-only masterKey', () => {
.then(done.fail)
.catch(res => {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
- expect(res.data.error).toBe("read-only masterKey isn't allowed to update the config.");
+ expect(res.data.error).toBe('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to update the config."));
done();
});
});
it('should throw when trying to send push', done => {
+ loggerErrorSpy.calls.reset();
request({
url: `${Parse.serverURL}/push`,
method: 'POST',
@@ -1133,8 +1166,9 @@ describe('read-only masterKey', () => {
.catch(res => {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe(
- "read-only masterKey isn't allowed to send push notifications."
+ 'Permission denied'
);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to send push notifications."));
done();
});
});
diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js
index 7891fa847e..5d92ef36e1 100644
--- a/spec/schemas.spec.js
+++ b/spec/schemas.spec.js
@@ -147,9 +147,14 @@ const masterKeyHeaders = {
};
describe('schemas', () => {
+ let loggerErrorSpy;
+
beforeEach(async () => {
await reconfigureServer();
config = Config.get('test');
+
+ const logger = require('../lib/logger').default;
+ loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
});
it('requires the master key to get all schemas', done => {
@@ -167,25 +172,29 @@ describe('schemas', () => {
});
it('requires the master key to get one schema', done => {
+ loggerErrorSpy.calls.reset();
request({
url: 'http://localhost:8378/1/schemas/SomeSchema',
json: true,
headers: restKeyHeaders,
}).then(fail, response => {
expect(response.status).toEqual(403);
- expect(response.data.error).toEqual('unauthorized: master key is required');
+ expect(response.data.error).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("unauthorized: master key is required"));
done();
});
});
it('asks for the master key if you use the rest key', done => {
+ loggerErrorSpy.calls.reset();
request({
url: 'http://localhost:8378/1/schemas',
json: true,
headers: restKeyHeaders,
}).then(fail, response => {
expect(response.status).toEqual(403);
- expect(response.data.error).toEqual('unauthorized: master key is required');
+ expect(response.data.error).toEqual('Permission denied');
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("unauthorized: master key is required"));
done();
});
});
@@ -1826,6 +1835,7 @@ describe('schemas', () => {
},
},
}).then(() => {
+ loggerErrorSpy.calls.reset();
const object = new Parse.Object('AClass');
object.set('hello', 'world');
return object.save().then(
@@ -1834,7 +1844,9 @@ describe('schemas', () => {
done();
},
err => {
- expect(err.message).toEqual('Permission denied for action addField on class AClass.');
+ expect(err.message).toEqual('Permission denied');
+ expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action addField on class AClass'));
done();
}
);
@@ -2198,13 +2210,16 @@ describe('schemas', () => {
});
})
.then(() => {
+ loggerErrorSpy.calls.reset();
const query = new Parse.Query('AClass');
return query.find().then(
() => {
fail('Use should hot be able to find!');
},
err => {
- expect(err.message).toEqual('Permission denied for action find on class AClass.');
+ expect(err.message).toEqual('Permission denied');
+ expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass'));
return Promise.resolve();
}
);
@@ -2258,13 +2273,16 @@ describe('schemas', () => {
});
})
.then(() => {
+ loggerErrorSpy.calls.reset();
const query = new Parse.Query('AClass');
return query.find().then(
() => {
fail('User should not be able to find!');
},
err => {
- expect(err.message).toEqual('Permission denied for action find on class AClass.');
+ expect(err.message).toEqual('Permission denied');
+ expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass'));
return Promise.resolve();
}
);
@@ -2343,13 +2361,16 @@ describe('schemas', () => {
});
})
.then(() => {
+ loggerErrorSpy.calls.reset();
const query = new Parse.Query('AClass');
return query.find().then(
() => {
fail('User should not be able to find!');
},
err => {
- expect(err.message).toEqual('Permission denied for action find on class AClass.');
+ expect(err.message).toEqual('Permission denied');
+ expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass'));
return Promise.resolve();
}
);
@@ -2419,13 +2440,16 @@ describe('schemas', () => {
});
})
.then(() => {
+ loggerErrorSpy.calls.reset();
const query = new Parse.Query('AClass');
return query.find().then(
() => {
fail('User should not be able to find!');
},
err => {
- expect(err.message).toEqual('Permission denied for action find on class AClass.');
+ expect(err.message).toEqual('Permission denied');
+ expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass'));
return Promise.resolve();
}
);
@@ -2450,13 +2474,16 @@ describe('schemas', () => {
);
})
.then(() => {
+ loggerErrorSpy.calls.reset();
const query = new Parse.Query('AClass');
return query.find().then(
() => {
fail('User should not be able to find!');
},
err => {
- expect(err.message).toEqual('Permission denied for action find on class AClass.');
+ expect(err.message).toEqual('Permission denied');
+ expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass'));
return Promise.resolve();
}
);
@@ -2531,6 +2558,7 @@ describe('schemas', () => {
return Parse.User.logIn('admin', 'admin');
})
.then(() => {
+ loggerErrorSpy.calls.reset();
const query = new Parse.Query('AClass');
return query.find();
})
@@ -2540,7 +2568,9 @@ describe('schemas', () => {
return Promise.resolve();
},
err => {
- expect(err.message).toEqual('Permission denied for action create on class AClass.');
+ expect(err.message).toEqual('Permission denied');
+ expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action create on class AClass'));
return Promise.resolve();
}
)
@@ -2548,6 +2578,7 @@ describe('schemas', () => {
return Parse.User.logIn('user2', 'user2');
})
.then(() => {
+ loggerErrorSpy.calls.reset();
const query = new Parse.Query('AClass');
return query.find();
})
@@ -2557,7 +2588,9 @@ describe('schemas', () => {
return Promise.resolve();
},
err => {
- expect(err.message).toEqual('Permission denied for action find on class AClass.');
+ expect(err.message).toEqual('Permission denied');
+ expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass'));
return Promise.resolve();
}
)
diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js
index 0d66c0a135..f3aff6ef0b 100644
--- a/spec/vulnerabilities.spec.js
+++ b/spec/vulnerabilities.spec.js
@@ -13,9 +13,13 @@ describe('Vulnerabilities', () => {
});
it('denies user creation with poisoned object ID', async () => {
+ const logger = require('../lib/logger').default;
+ const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
+ loggerErrorSpy.calls.reset();
await expectAsync(
new Parse.User({ id: 'role:a', username: 'a', password: '123' }).save()
- ).toBeRejectedWith(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.'));
+ ).toBeRejectedWith(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied'));
+ expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Invalid object ID."));
});
describe('existing sessions for users with poisoned object ID', () => {
diff --git a/src/Adapters/Analytics/AnalyticsAdapter.js b/src/Adapters/Analytics/AnalyticsAdapter.js
index e3cced14f5..380d869a5e 100644
--- a/src/Adapters/Analytics/AnalyticsAdapter.js
+++ b/src/Adapters/Analytics/AnalyticsAdapter.js
@@ -1,4 +1,4 @@
-/*eslint no-unused-vars: "off"*/
+/* eslint-disable unused-imports/no-unused-vars */
/**
* @interface AnalyticsAdapter
* @module Adapters
diff --git a/src/Adapters/Auth/AuthAdapter.js b/src/Adapters/Auth/AuthAdapter.js
index afc05d0bb2..ebcea4d3d8 100644
--- a/src/Adapters/Auth/AuthAdapter.js
+++ b/src/Adapters/Auth/AuthAdapter.js
@@ -1,4 +1,4 @@
-/*eslint no-unused-vars: "off"*/
+/* eslint-disable unused-imports/no-unused-vars */
/**
* @interface ParseAuthResponse
diff --git a/src/Adapters/Auth/apple.js b/src/Adapters/Auth/apple.js
index 24502f4a55..7c9a845e95 100644
--- a/src/Adapters/Auth/apple.js
+++ b/src/Adapters/Auth/apple.js
@@ -63,7 +63,7 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
let key;
try {
key = await authUtils.getSigningKey(client, keyId);
- } catch (error) {
+ } catch {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Unable to find matching key for Key ID: ${keyId}`
diff --git a/src/Adapters/Auth/facebook.js b/src/Adapters/Auth/facebook.js
index 273004ad62..d9b6589ab5 100644
--- a/src/Adapters/Auth/facebook.js
+++ b/src/Adapters/Auth/facebook.js
@@ -122,7 +122,7 @@ const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
let key;
try {
key = await authUtils.getSigningKey(client, keyId);
- } catch (error) {
+ } catch {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Unable to find matching key for Key ID: ${keyId}`
diff --git a/src/Adapters/Cache/CacheAdapter.js b/src/Adapters/Cache/CacheAdapter.js
index 7632797e55..9a84f89ec6 100644
--- a/src/Adapters/Cache/CacheAdapter.js
+++ b/src/Adapters/Cache/CacheAdapter.js
@@ -1,4 +1,4 @@
-/*eslint no-unused-vars: "off"*/
+/* eslint-disable unused-imports/no-unused-vars */
/**
* @interface
* @memberof module:Adapters
diff --git a/src/Adapters/Email/MailAdapter.js b/src/Adapters/Email/MailAdapter.js
index 93069e2c27..10e77232b4 100644
--- a/src/Adapters/Email/MailAdapter.js
+++ b/src/Adapters/Email/MailAdapter.js
@@ -1,4 +1,4 @@
-/*eslint no-unused-vars: "off"*/
+/* eslint-disable unused-imports/no-unused-vars */
/**
* @interface
* @memberof module:Adapters
diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js
index f06c52df89..0e9b555853 100644
--- a/src/Adapters/Files/FilesAdapter.js
+++ b/src/Adapters/Files/FilesAdapter.js
@@ -1,4 +1,4 @@
-/*eslint no-unused-vars: "off"*/
+/* eslint-disable unused-imports/no-unused-vars */
// Files Adapter
//
// Allows you to change the file storage mechanism.
diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js
index 45a585ecc2..6820fc887f 100644
--- a/src/Adapters/Files/GridFSBucketAdapter.js
+++ b/src/Adapters/Files/GridFSBucketAdapter.js
@@ -9,7 +9,7 @@
// @flow-disable-next
import { MongoClient, GridFSBucket, Db } from 'mongodb';
import { FilesAdapter, validateFilename } from './FilesAdapter';
-import defaults from '../../defaults';
+import defaults, { ParseServerDatabaseOptions } from '../../defaults';
const crypto = require('crypto');
export class GridFSBucketAdapter extends FilesAdapter {
@@ -34,10 +34,10 @@ export class GridFSBucketAdapter extends FilesAdapter {
.digest('base64')
.substring(0, 32)
: null;
- const defaultMongoOptions = {
- };
+ const defaultMongoOptions = {};
const _mongoOptions = Object.assign(defaultMongoOptions, mongoOptions);
- for (const key of ['enableSchemaHooks', 'schemaCacheTtl', 'maxTimeMS', 'disableIndexFieldValidation']) {
+ // Remove Parse Server-specific options that should not be passed to MongoDB client
+ for (const key of ParseServerDatabaseOptions) {
delete _mongoOptions[key];
}
this._mongoOptions = _mongoOptions;
@@ -171,7 +171,7 @@ export class GridFSBucketAdapter extends FilesAdapter {
fileNamesNotRotated = fileNamesNotRotated.filter(function (value) {
return value !== fileName;
});
- } catch (err) {
+ } catch {
continue;
}
}
diff --git a/src/Adapters/Logger/LoggerAdapter.js b/src/Adapters/Logger/LoggerAdapter.js
index 3853d5f480..957719be9b 100644
--- a/src/Adapters/Logger/LoggerAdapter.js
+++ b/src/Adapters/Logger/LoggerAdapter.js
@@ -1,4 +1,4 @@
-/*eslint no-unused-vars: "off"*/
+/* eslint-disable unused-imports/no-unused-vars */
/**
* @interface
* @memberof module:Adapters
diff --git a/src/Adapters/Logger/WinstonLogger.js b/src/Adapters/Logger/WinstonLogger.js
index fe28660056..886a1394f3 100644
--- a/src/Adapters/Logger/WinstonLogger.js
+++ b/src/Adapters/Logger/WinstonLogger.js
@@ -42,7 +42,7 @@ function configureTransports(options) {
parseServerError.name = 'parse-server-error';
transports.push(parseServerError);
}
- } catch (e) {
+ } catch {
/* */
}
@@ -86,7 +86,7 @@ export function configureLogger({
}
try {
fs.mkdirSync(logsFolder);
- } catch (e) {
+ } catch {
/* */
}
}
diff --git a/src/Adapters/PubSub/PubSubAdapter.js b/src/Adapters/PubSub/PubSubAdapter.js
index 728dff90e8..b68d9bee5d 100644
--- a/src/Adapters/PubSub/PubSubAdapter.js
+++ b/src/Adapters/PubSub/PubSubAdapter.js
@@ -1,4 +1,4 @@
-/*eslint no-unused-vars: "off"*/
+/* eslint-disable unused-imports/no-unused-vars */
/**
* @interface
* @memberof module:Adapters
diff --git a/src/Adapters/Push/PushAdapter.js b/src/Adapters/Push/PushAdapter.js
index fb0adbf469..d6862b00c3 100644
--- a/src/Adapters/Push/PushAdapter.js
+++ b/src/Adapters/Push/PushAdapter.js
@@ -1,5 +1,5 @@
+/* eslint-disable unused-imports/no-unused-vars */
// @flow
-/*eslint no-unused-vars: "off"*/
// Push Adapter
//
// Allows you to change the push notification mechanism.
diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js
index 39b335d52e..50fd348861 100644
--- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js
+++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js
@@ -1,23 +1,24 @@
// @flow
+import { format as formatUrl, parse as parseUrl } from '../../../vendor/mongodbUrl';
+import type { QueryOptions, QueryType, SchemaType, StorageClass } from '../StorageAdapter';
+import { StorageAdapter } from '../StorageAdapter';
import MongoCollection from './MongoCollection';
import MongoSchemaCollection from './MongoSchemaCollection';
-import { StorageAdapter } from '../StorageAdapter';
-import type { SchemaType, QueryType, StorageClass, QueryOptions } from '../StorageAdapter';
-import { parse as parseUrl, format as formatUrl } from '../../../vendor/mongodbUrl';
import {
- parseObjectToMongoObjectForCreate,
mongoObjectToParseObject,
+ parseObjectToMongoObjectForCreate,
transformKey,
- transformWhere,
- transformUpdate,
transformPointerString,
+ transformUpdate,
+ transformWhere,
} from './MongoTransform';
// @flow-disable-next
import Parse from 'parse/node';
// @flow-disable-next
import _ from 'lodash';
-import defaults from '../../../defaults';
+import defaults, { ParseServerDatabaseOptions } from '../../../defaults';
import logger from '../../../logger';
+import Utils from '../../../Utils';
// @flow-disable-next
const mongodb = require('mongodb');
@@ -132,6 +133,7 @@ export class MongoStorageAdapter implements StorageAdapter {
_mongoOptions: Object;
_onchange: any;
_stream: any;
+ _logClientEvents: ?Array;
// Public
connectionPromise: ?Promise;
database: any;
@@ -145,8 +147,7 @@ export class MongoStorageAdapter implements StorageAdapter {
constructor({ uri = defaults.DefaultMongoURI, collectionPrefix = '', mongoOptions = {} }: any) {
this._uri = uri;
this._collectionPrefix = collectionPrefix;
- this._mongoOptions = { ...mongoOptions };
- this._onchange = () => { };
+ this._onchange = () => {};
// MaxTimeMS is not a global MongoDB client option, it is applied per operation.
this._maxTimeMS = mongoOptions.maxTimeMS;
@@ -154,22 +155,14 @@ export class MongoStorageAdapter implements StorageAdapter {
this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks;
this.schemaCacheTtl = mongoOptions.schemaCacheTtl;
this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation;
- // Remove Parse Server-specific options that should not be passed to MongoDB client
- // Note: We only delete from this._mongoOptions, not from the original mongoOptions object,
- // because other components (like DatabaseController) need access to these options
- for (const key of [
- 'enableSchemaHooks',
- 'schemaCacheTtl',
- 'maxTimeMS',
- 'disableIndexFieldValidation',
- 'createIndexUserUsername',
- 'createIndexUserUsernameCaseInsensitive',
- 'createIndexUserEmail',
- 'createIndexUserEmailCaseInsensitive',
- 'createIndexUserEmailVerifyToken',
- 'createIndexUserPasswordResetToken',
- 'createIndexRoleName',
- ]) {
+ this._logClientEvents = mongoOptions.logClientEvents;
+
+ // Create a copy of mongoOptions and remove Parse Server-specific options that should not
+ // be passed to MongoDB client. Note: We only delete from this._mongoOptions, not from the
+ // original mongoOptions object, because other components (like DatabaseController) need
+ // access to these options.
+ this._mongoOptions = { ...mongoOptions };
+ for (const key of ParseServerDatabaseOptions) {
delete this._mongoOptions[key];
}
}
@@ -203,6 +196,31 @@ export class MongoStorageAdapter implements StorageAdapter {
client.on('close', () => {
delete this.connectionPromise;
});
+
+ // Set up client event logging if configured
+ if (this._logClientEvents && Array.isArray(this._logClientEvents)) {
+ this._logClientEvents.forEach(eventConfig => {
+ client.on(eventConfig.name, event => {
+ let logData = {};
+ if (!eventConfig.keys || eventConfig.keys.length === 0) {
+ logData = event;
+ } else {
+ eventConfig.keys.forEach(keyPath => {
+ logData[keyPath] = _.get(event, keyPath);
+ });
+ }
+
+ // Validate log level exists, fallback to 'info'
+ const logLevel = typeof logger[eventConfig.logLevel] === 'function' ? eventConfig.logLevel : 'info';
+
+ // Safe JSON serialization with Map/Set and circular reference support
+ const logMessage = `MongoDB client event ${eventConfig.name}: ${JSON.stringify(logData, Utils.getCircularReplacer())}`;
+
+ logger[logLevel](logMessage);
+ });
+ });
+ }
+
this.client = client;
this.database = database;
})
diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js
index f78c972bdc..34481a090b 100644
--- a/src/Adapters/Storage/Mongo/MongoTransform.js
+++ b/src/Adapters/Storage/Mongo/MongoTransform.js
@@ -305,7 +305,7 @@ function transformQueryKeyValue(className, key, value, schema, count = false) {
default: {
// Other auth data
const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/);
- if (authDataMatch) {
+ if (authDataMatch && className === '_User') {
const provider = authDataMatch[1];
// Special-case auth data.
return { key: `_auth_data_${provider}.id`, value };
diff --git a/src/Adapters/WebSocketServer/WSAdapter.js b/src/Adapters/WebSocketServer/WSAdapter.js
index 5522dad365..db35d928bf 100644
--- a/src/Adapters/WebSocketServer/WSAdapter.js
+++ b/src/Adapters/WebSocketServer/WSAdapter.js
@@ -1,4 +1,4 @@
-/*eslint no-unused-vars: "off"*/
+/* eslint-disable unused-imports/no-unused-vars */
import { WSSAdapter } from './WSSAdapter';
const WebSocketServer = require('ws').Server;
diff --git a/src/Adapters/WebSocketServer/WSSAdapter.js b/src/Adapters/WebSocketServer/WSSAdapter.js
index a810c03f9d..0831828f3c 100644
--- a/src/Adapters/WebSocketServer/WSSAdapter.js
+++ b/src/Adapters/WebSocketServer/WSSAdapter.js
@@ -1,4 +1,4 @@
-/*eslint no-unused-vars: "off"*/
+/* eslint-disable unused-imports/no-unused-vars */
// WebSocketServer Adapter
//
// Adapter classes must implement the following functions:
diff --git a/src/Config.js b/src/Config.js
index bf6d50626c..241edf9771 100644
--- a/src/Config.js
+++ b/src/Config.js
@@ -32,6 +32,11 @@ function removeTrailingSlash(str) {
return str;
}
+/**
+ * Config keys that need to be loaded asynchronously.
+ */
+const asyncKeys = ['publicServerURL'];
+
export class Config {
static get(applicationId: string, mount: string) {
const cacheInfo = AppCache.get(applicationId);
@@ -56,9 +61,42 @@ export class Config {
return config;
}
+ async loadKeys() {
+ await Promise.all(
+ asyncKeys.map(async key => {
+ if (typeof this[`_${key}`] === 'function') {
+ try {
+ this[key] = await this[`_${key}`]();
+ } catch (error) {
+ throw new Error(`Failed to resolve async config key '${key}': ${error.message}`);
+ }
+ }
+ })
+ );
+
+ const cachedConfig = AppCache.get(this.appId);
+ if (cachedConfig) {
+ const updatedConfig = { ...cachedConfig };
+ asyncKeys.forEach(key => {
+ updatedConfig[key] = this[key];
+ });
+ AppCache.put(this.appId, updatedConfig);
+ }
+ }
+
+ static transformConfiguration(serverConfiguration) {
+ for (const key of Object.keys(serverConfiguration)) {
+ if (asyncKeys.includes(key) && typeof serverConfiguration[key] === 'function') {
+ serverConfiguration[`_${key}`] = serverConfiguration[key];
+ delete serverConfiguration[key];
+ }
+ }
+ }
+
static put(serverConfiguration) {
Config.validateOptions(serverConfiguration);
Config.validateControllers(serverConfiguration);
+ Config.transformConfiguration(serverConfiguration);
AppCache.put(serverConfiguration.appId, serverConfiguration);
Config.setupPasswordValidator(serverConfiguration.passwordPolicy);
return serverConfiguration;
@@ -115,11 +153,7 @@ export class Config {
throw 'extendSessionOnUse must be a boolean value';
}
- if (publicServerURL) {
- if (!publicServerURL.startsWith('http://') && !publicServerURL.startsWith('https://')) {
- throw 'publicServerURL should be a valid HTTPS URL starting with https://';
- }
- }
+ this.validatePublicServerURL({ publicServerURL });
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
this.validateIps('masterKeyIps', masterKeyIps);
this.validateIps('maintenanceKeyIps', maintenanceKeyIps);
@@ -154,6 +188,7 @@ export class Config {
userController,
appName,
publicServerURL,
+ _publicServerURL,
emailVerifyTokenValidityDuration,
emailVerifyTokenReuseIfValid,
}) {
@@ -162,7 +197,7 @@ export class Config {
this.validateEmailConfiguration({
emailAdapter,
appName,
- publicServerURL,
+ publicServerURL: publicServerURL || _publicServerURL,
emailVerifyTokenValidityDuration,
emailVerifyTokenReuseIfValid,
});
@@ -432,6 +467,30 @@ export class Config {
}
}
+ static validatePublicServerURL({ publicServerURL, required = false }) {
+ if (!publicServerURL) {
+ if (!required) {
+ return;
+ }
+ throw 'The option publicServerURL is required.';
+ }
+
+ const type = typeof publicServerURL;
+
+ if (type === 'string') {
+ if (!publicServerURL.startsWith('http://') && !publicServerURL.startsWith('https://')) {
+ throw 'The option publicServerURL must be a valid URL starting with http:// or https://.';
+ }
+ return;
+ }
+
+ if (type === 'function') {
+ return;
+ }
+
+ throw `The option publicServerURL must be a string or function, but got ${type}.`;
+ }
+
static validateEmailConfiguration({
emailAdapter,
appName,
@@ -445,9 +504,7 @@ export class Config {
if (typeof appName !== 'string') {
throw 'An app name is required for e-mail verification and password resets.';
}
- if (typeof publicServerURL !== 'string') {
- throw 'A public server url is required for e-mail verification and password resets.';
- }
+ this.validatePublicServerURL({ publicServerURL, required: true });
if (emailVerifyTokenValidityDuration) {
if (isNaN(emailVerifyTokenValidityDuration)) {
throw 'Email verify token validity duration must be a valid number.';
@@ -602,6 +659,11 @@ export class Config {
} else if (typeof databaseOptions.schemaCacheTtl !== 'number') {
throw `databaseOptions.schemaCacheTtl must be a number`;
}
+ if (databaseOptions.allowPublicExplain === undefined) {
+ databaseOptions.allowPublicExplain = DatabaseOptions.allowPublicExplain.default;
+ } else if (typeof databaseOptions.allowPublicExplain !== 'boolean') {
+ throw `Parse Server option 'databaseOptions.allowPublicExplain' must be a boolean.`;
+ }
}
static validateRateLimit(rateLimit) {
@@ -757,7 +819,6 @@ export class Config {
return this.masterKey;
}
-
// TODO: Remove this function once PagesRouter replaces the PublicAPIRouter;
// the (default) endpoint has to be defined in PagesRouter only.
get pagesEndpoint() {
diff --git a/src/Controllers/HooksController.js b/src/Controllers/HooksController.js
index fef01946f6..6497784754 100644
--- a/src/Controllers/HooksController.js
+++ b/src/Controllers/HooksController.js
@@ -225,7 +225,7 @@ function wrapToHTTPRequest(hook, key) {
if (typeof body === 'string') {
try {
body = JSON.parse(body);
- } catch (e) {
+ } catch {
err = {
error: 'Malformed response',
code: -1,
diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js
index fccadd23ce..b605fba632 100644
--- a/src/Controllers/SchemaController.js
+++ b/src/Controllers/SchemaController.js
@@ -20,6 +20,7 @@ import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
import SchemaCache from '../Adapters/Cache/SchemaCache';
import DatabaseController from './DatabaseController';
import Config from '../Config';
+import { createSanitizedError } from '../Error';
// @flow-disable-next
import deepcopy from 'deepcopy';
import type {
@@ -1398,19 +1399,22 @@ export default class SchemaController {
return true;
}
const perms = classPermissions[operation];
+ const config = Config.get(Parse.applicationId)
// If only for authenticated users
// make sure we have an aclGroup
if (perms['requiresAuthentication']) {
// If aclGroup has * (public)
if (!aclGroup || aclGroup.length == 0) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OBJECT_NOT_FOUND,
- 'Permission denied, user needs to be authenticated.'
+ 'Permission denied, user needs to be authenticated.',
+ config
);
} else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OBJECT_NOT_FOUND,
- 'Permission denied, user needs to be authenticated.'
+ 'Permission denied, user needs to be authenticated.',
+ config
);
}
// requiresAuthentication passed, just move forward
@@ -1425,9 +1429,10 @@ export default class SchemaController {
// Reject create when write lockdown
if (permissionField == 'writeUserFields' && operation == 'create') {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- `Permission denied for action ${operation} on class ${className}.`
+ `Permission denied for action ${operation} on class ${className}.`,
+ config
);
}
@@ -1448,9 +1453,10 @@ export default class SchemaController {
}
}
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- `Permission denied for action ${operation} on class ${className}.`
+ `Permission denied for action ${operation} on class ${className}.`,
+ config
);
}
diff --git a/src/Controllers/index.js b/src/Controllers/index.js
index abf0950640..9397dac561 100644
--- a/src/Controllers/index.js
+++ b/src/Controllers/index.js
@@ -217,7 +217,7 @@ export function getDatabaseAdapter(databaseURI, collectionPrefix, databaseOption
try {
const parsedURI = new URL(databaseURI);
protocol = parsedURI.protocol ? parsedURI.protocol.toLowerCase() : null;
- } catch (e) {
+ } catch {
/* */
}
switch (protocol) {
diff --git a/src/Deprecator/Deprecations.js b/src/Deprecator/Deprecations.js
index 970364432b..c63225f5b5 100644
--- a/src/Deprecator/Deprecations.js
+++ b/src/Deprecator/Deprecations.js
@@ -18,4 +18,5 @@
module.exports = [
{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
{ optionKey: 'enableInsecureAuthAdapters', changeNewDefault: 'false' },
+ { optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' },
];
diff --git a/src/Deprecator/Deprecator.js b/src/Deprecator/Deprecator.js
index 27033c946d..4744efbdd8 100644
--- a/src/Deprecator/Deprecator.js
+++ b/src/Deprecator/Deprecator.js
@@ -1,5 +1,6 @@
import logger from '../logger';
import Deprecations from './Deprecations';
+import Utils from '../Utils';
/**
* The deprecator class.
@@ -21,7 +22,7 @@ class Deprecator {
const changeNewDefault = deprecation.changeNewDefault;
// If default will change, only throw a warning if option is not set
- if (changeNewDefault != null && options[optionKey] == null) {
+ if (changeNewDefault != null && Utils.getNestedProperty(options, optionKey) == null) {
Deprecator._logOption({ optionKey, changeNewDefault, solution });
}
}
diff --git a/src/Error.js b/src/Error.js
new file mode 100644
index 0000000000..75eff3d673
--- /dev/null
+++ b/src/Error.js
@@ -0,0 +1,44 @@
+import defaultLogger from './logger';
+
+/**
+ * Creates a sanitized error that hides detailed information from clients
+ * while logging the detailed message server-side.
+ *
+ * @param {number} errorCode - The Parse.Error code (e.g., Parse.Error.OPERATION_FORBIDDEN)
+ * @param {string} detailedMessage - The detailed error message to log server-side
+ * @returns {Parse.Error} A Parse.Error with sanitized message
+ */
+function createSanitizedError(errorCode, detailedMessage, config) {
+ // On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file
+ if (process.env.TESTING) {
+ defaultLogger.error('Sanitized error:', detailedMessage);
+ } else {
+ defaultLogger.error(detailedMessage);
+ }
+
+ return new Parse.Error(errorCode, config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage);
+}
+
+/**
+ * Creates a sanitized error from a regular Error object
+ * Used for non-Parse.Error errors (e.g., Express errors)
+ *
+ * @param {number} statusCode - HTTP status code (e.g., 403)
+ * @param {string} detailedMessage - The detailed error message to log server-side
+ * @returns {Error} An Error with sanitized message
+ */
+function createSanitizedHttpError(statusCode, detailedMessage, config) {
+ // On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file
+ if (process.env.TESTING) {
+ defaultLogger.error('Sanitized error:', detailedMessage);
+ } else {
+ defaultLogger.error(detailedMessage);
+ }
+
+ const error = new Error();
+ error.status = statusCode;
+ error.message = config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage;
+ return error;
+}
+
+export { createSanitizedError, createSanitizedHttpError };
diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js
index bf7e14f7e2..231e44f5ef 100644
--- a/src/GraphQL/ParseGraphQLServer.js
+++ b/src/GraphQL/ParseGraphQLServer.js
@@ -97,21 +97,38 @@ class ParseGraphQLServer {
if (schemaRef === newSchemaRef && this._server) {
return this._server;
}
- const { schema, context } = await this._getGraphQLOptions();
- const apollo = new ApolloServer({
- csrfPrevention: {
- // See https://www.apollographql.com/docs/router/configuration/csrf/
- // needed since we use graphql upload
- requestHeaders: ['X-Parse-Application-Id'],
- },
- introspection: this.config.graphQLPublicIntrospection,
- plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection)],
- schema,
- });
- await apollo.start();
- this._server = expressMiddleware(apollo, {
- context,
- });
+ // It means a parallel _getServer call is already in progress
+ if (this._schemaRefMutex === newSchemaRef) {
+ return this._server;
+ }
+ // Update the schema ref mutex to avoid parallel _getServer calls
+ this._schemaRefMutex = newSchemaRef;
+ const createServer = async () => {
+ try {
+ const { schema, context } = await this._getGraphQLOptions();
+ const apollo = new ApolloServer({
+ csrfPrevention: {
+ // See https://www.apollographql.com/docs/router/configuration/csrf/
+ // needed since we use graphql upload
+ requestHeaders: ['X-Parse-Application-Id'],
+ },
+ introspection: this.config.graphQLPublicIntrospection,
+ plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection)],
+ schema,
+ });
+ await apollo.start();
+ return expressMiddleware(apollo, {
+ context,
+ });
+ } catch (e) {
+ // Reset all mutexes and forward the error
+ this._server = null;
+ this._schemaRefMutex = null;
+ throw e;
+ }
+ }
+ // Do not await so parallel request will wait the same promise ref
+ this._server = createServer();
return this._server;
}
diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js
index 0a16a1c4a6..8439dfeb4f 100644
--- a/src/GraphQL/loaders/filesMutations.js
+++ b/src/GraphQL/loaders/filesMutations.js
@@ -38,7 +38,7 @@ const handleUpload = async (upload, config) => {
res.on('end', () => {
try {
resolve(JSON.parse(data));
- } catch (e) {
+ } catch {
reject(new Parse.Error(Parse.error, data));
}
});
diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js
index ffb4d6523b..93cd89d54a 100644
--- a/src/GraphQL/loaders/schemaMutations.js
+++ b/src/GraphQL/loaders/schemaMutations.js
@@ -6,6 +6,7 @@ import * as schemaTypes from './schemaTypes';
import { transformToParse, transformToGraphQL } from '../transformers/schemaFields';
import { enforceMasterKeyAccess } from '../parseGraphQLUtils';
import { getClass } from './schemaQueries';
+import { createSanitizedError } from '../../Error';
const load = parseGraphQLSchema => {
const createClassMutation = mutationWithClientMutationId({
@@ -30,12 +31,13 @@ const load = parseGraphQLSchema => {
const { name, schemaFields } = deepcopy(args);
const { config, auth } = context;
- enforceMasterKeyAccess(auth);
+ enforceMasterKeyAccess(auth, config);
if (auth.isReadOnly) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- "read-only masterKey isn't allowed to create a schema."
+ "read-only masterKey isn't allowed to create a schema.",
+ config
);
}
@@ -79,12 +81,13 @@ const load = parseGraphQLSchema => {
const { name, schemaFields } = deepcopy(args);
const { config, auth } = context;
- enforceMasterKeyAccess(auth);
+ enforceMasterKeyAccess(auth, config);
if (auth.isReadOnly) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- "read-only masterKey isn't allowed to update a schema."
+ "read-only masterKey isn't allowed to update a schema.",
+ config
);
}
@@ -130,12 +133,13 @@ const load = parseGraphQLSchema => {
const { name } = deepcopy(args);
const { config, auth } = context;
- enforceMasterKeyAccess(auth);
+ enforceMasterKeyAccess(auth, config);
if (auth.isReadOnly) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- "read-only masterKey isn't allowed to delete a schema."
+ "read-only masterKey isn't allowed to delete a schema.",
+ config
);
}
diff --git a/src/GraphQL/loaders/schemaQueries.js b/src/GraphQL/loaders/schemaQueries.js
index 25bc071919..2956b47934 100644
--- a/src/GraphQL/loaders/schemaQueries.js
+++ b/src/GraphQL/loaders/schemaQueries.js
@@ -31,7 +31,7 @@ const load = parseGraphQLSchema => {
const { name } = deepcopy(args);
const { config, auth } = context;
- enforceMasterKeyAccess(auth);
+ enforceMasterKeyAccess(auth, config);
const schema = await config.database.loadSchema({ clearCache: true });
const parseClass = await getClass(name, schema);
@@ -57,7 +57,7 @@ const load = parseGraphQLSchema => {
try {
const { config, auth } = context;
- enforceMasterKeyAccess(auth);
+ enforceMasterKeyAccess(auth, config);
const schema = await config.database.loadSchema({ clearCache: true });
return (await schema.getAllClasses(true)).map(parseClass => ({
diff --git a/src/GraphQL/loaders/usersQueries.js b/src/GraphQL/loaders/usersQueries.js
index c64ce6b90d..dc9f57f5ef 100644
--- a/src/GraphQL/loaders/usersQueries.js
+++ b/src/GraphQL/loaders/usersQueries.js
@@ -4,11 +4,12 @@ import Parse from 'parse/node';
import rest from '../../rest';
import { extractKeysAndInclude } from './parseClassTypes';
import { Auth } from '../../Auth';
+import { createSanitizedError } from '../../Error';
const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) => {
const { info, config } = context;
if (!info || !info.sessionToken) {
- throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
+ throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
}
const sessionToken = info.sessionToken;
const selectedFields = getFieldNames(queryInfo)
@@ -62,7 +63,7 @@ const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) =
info.context
);
if (!response.results || response.results.length == 0) {
- throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
+ throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
} else {
const user = response.results[0];
return {
diff --git a/src/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js
index f1194784cb..ba5fd1b416 100644
--- a/src/GraphQL/parseGraphQLUtils.js
+++ b/src/GraphQL/parseGraphQLUtils.js
@@ -1,9 +1,14 @@
import Parse from 'parse/node';
import { GraphQLError } from 'graphql';
+import { createSanitizedError } from '../Error';
-export function enforceMasterKeyAccess(auth) {
+export function enforceMasterKeyAccess(auth, config) {
if (!auth.isMaster) {
- throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'unauthorized: master key is required');
+ throw createSanitizedError(
+ Parse.Error.OPERATION_FORBIDDEN,
+ 'unauthorized: master key is required',
+ config
+ );
}
}
diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js
index d5674eaf29..774b4505e1 100644
--- a/src/Options/Definitions.js
+++ b/src/Options/Definitions.js
@@ -247,6 +247,13 @@ module.exports.ParseServerOptions = {
action: parsers.booleanParser,
default: true,
},
+ enableSanitizedErrorResponse: {
+ env: 'PARSE_SERVER_ENABLE_SANITIZED_ERROR_RESPONSE',
+ help:
+ 'If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`.',
+ action: parsers.booleanParser,
+ default: true,
+ },
encodeParseObjectInCloudFunction: {
env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION',
help:
@@ -495,7 +502,8 @@ module.exports.ParseServerOptions = {
},
publicServerURL: {
env: 'PARSE_PUBLIC_SERVER_URL',
- help: 'Public URL to your parse server with http:// or https://.',
+ help:
+ 'Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`.',
},
push: {
env: 'PARSE_SERVER_PUSH',
@@ -689,7 +697,8 @@ module.exports.RateLimitOptions = {
zone: {
env: 'PARSE_SERVER_RATE_LIMIT_ZONE',
help:
- "The type of rate limit to apply. The following types are supported:
- `global`: rate limit based on the number of requests made by all users
- `ip`: rate limit based on the IP address of the request
- `user`: rate limit based on the user ID of the request
- `session`: rate limit based on the session token of the request
:default: 'ip'",
+ 'The type of rate limit to apply. The following types are supported:- `global`: rate limit based on the number of requests made by all users
- `ip`: rate limit based on the IP address of the request
- `user`: rate limit based on the user ID of the request
- `session`: rate limit based on the session token of the request
Default is `ip`.',
+ default: 'ip',
},
};
module.exports.SecurityOptions = {
@@ -1082,7 +1091,88 @@ module.exports.FileUploadOptions = {
default: ['^(?![xXsS]?[hH][tT][mM][lL]?$)'],
},
};
+/* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'silly'` - Silly level (lowest priority) */
+module.exports.LogLevel = {
+ debug: {
+ env: 'PARSE_SERVER_LOG_LEVEL_DEBUG',
+ help: 'Debug level',
+ required: true,
+ },
+ error: {
+ env: 'PARSE_SERVER_LOG_LEVEL_ERROR',
+ help: 'Error level - highest priority',
+ required: true,
+ },
+ info: {
+ env: 'PARSE_SERVER_LOG_LEVEL_INFO',
+ help: 'Info level - default',
+ required: true,
+ },
+ silly: {
+ env: 'PARSE_SERVER_LOG_LEVEL_SILLY',
+ help: 'Silly level - lowest priority',
+ required: true,
+ },
+ verbose: {
+ env: 'PARSE_SERVER_LOG_LEVEL_VERBOSE',
+ help: 'Verbose level',
+ required: true,
+ },
+ warn: {
+ env: 'PARSE_SERVER_LOG_LEVEL_WARN',
+ help: 'Warning level',
+ required: true,
+ },
+};
+module.exports.LogClientEvent = {
+ keys: {
+ env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_KEYS',
+ help:
+ 'Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged.',
+ action: parsers.arrayParser,
+ },
+ logLevel: {
+ env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_LOG_LEVEL',
+ help:
+ "The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`.",
+ default: 'info',
+ },
+ name: {
+ env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_NAME',
+ help:
+ 'The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events.',
+ required: true,
+ },
+};
module.exports.DatabaseOptions = {
+ allowPublicExplain: {
+ env: 'PARSE_SERVER_DATABASE_ALLOW_PUBLIC_EXPLAIN',
+ help:
+ 'Set to `true` to allow `Parse.Query.explain` without master key.
\u26A0\uFE0F Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes.',
+ action: parsers.booleanParser,
+ default: true,
+ },
+ appName: {
+ env: 'PARSE_SERVER_DATABASE_APP_NAME',
+ help:
+ 'The MongoDB driver option to specify the name of the application that created this MongoClient instance.',
+ },
+ authMechanism: {
+ env: 'PARSE_SERVER_DATABASE_AUTH_MECHANISM',
+ help:
+ 'The MongoDB driver option to specify the authentication mechanism that MongoDB will use to authenticate the connection.',
+ },
+ authMechanismProperties: {
+ env: 'PARSE_SERVER_DATABASE_AUTH_MECHANISM_PROPERTIES',
+ help:
+ 'The MongoDB driver option to specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs.',
+ action: parsers.objectParser,
+ },
+ authSource: {
+ env: 'PARSE_SERVER_DATABASE_AUTH_SOURCE',
+ help:
+ "The MongoDB driver option to specify the database name associated with the user's credentials.",
+ },
autoSelectFamily: {
env: 'PARSE_SERVER_DATABASE_AUTO_SELECT_FAMILY',
help:
@@ -1095,6 +1185,11 @@ module.exports.DatabaseOptions = {
'The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead.',
action: parsers.numberParser('autoSelectFamilyAttemptTimeout'),
},
+ compressors: {
+ env: 'PARSE_SERVER_DATABASE_COMPRESSORS',
+ help:
+ 'The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance.',
+ },
connectTimeoutMS: {
env: 'PARSE_SERVER_DATABASE_CONNECT_TIMEOUT_MS',
help:
@@ -1150,6 +1245,12 @@ module.exports.DatabaseOptions = {
action: parsers.booleanParser,
default: true,
},
+ directConnection: {
+ env: 'PARSE_SERVER_DATABASE_DIRECT_CONNECTION',
+ help:
+ 'The MongoDB driver option to force a Single topology type with a connection string containing one host.',
+ action: parsers.booleanParser,
+ },
disableIndexFieldValidation: {
env: 'PARSE_SERVER_DATABASE_DISABLE_INDEX_FIELD_VALIDATION',
help:
@@ -1163,6 +1264,47 @@ module.exports.DatabaseOptions = {
action: parsers.booleanParser,
default: false,
},
+ forceServerObjectId: {
+ env: 'PARSE_SERVER_DATABASE_FORCE_SERVER_OBJECT_ID',
+ help: 'The MongoDB driver option to force server to assign _id values instead of driver.',
+ action: parsers.booleanParser,
+ },
+ heartbeatFrequencyMS: {
+ env: 'PARSE_SERVER_DATABASE_HEARTBEAT_FREQUENCY_MS',
+ help:
+ 'The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment.',
+ action: parsers.numberParser('heartbeatFrequencyMS'),
+ },
+ loadBalanced: {
+ env: 'PARSE_SERVER_DATABASE_LOAD_BALANCED',
+ help:
+ 'The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service.',
+ action: parsers.booleanParser,
+ },
+ localThresholdMS: {
+ env: 'PARSE_SERVER_DATABASE_LOCAL_THRESHOLD_MS',
+ help:
+ 'The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances.',
+ action: parsers.numberParser('localThresholdMS'),
+ },
+ logClientEvents: {
+ env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS',
+ help: 'An array of MongoDB client event configurations to enable logging of specific events.',
+ action: parsers.arrayParser,
+ type: 'LogClientEvent[]',
+ },
+ maxConnecting: {
+ env: 'PARSE_SERVER_DATABASE_MAX_CONNECTING',
+ help:
+ '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.',
+ action: parsers.numberParser('maxIdleTimeMS'),
+ },
maxPoolSize: {
env: 'PARSE_SERVER_DATABASE_MAX_POOL_SIZE',
help:
@@ -1187,6 +1329,51 @@ module.exports.DatabaseOptions = {
'The MongoDB driver option to set the minimum number of opened, cached, ready-to-use database connections maintained by the driver.',
action: parsers.numberParser('minPoolSize'),
},
+ proxyHost: {
+ env: 'PARSE_SERVER_DATABASE_PROXY_HOST',
+ help:
+ 'The MongoDB driver option to configure a Socks5 proxy host used for creating TCP connections.',
+ },
+ proxyPassword: {
+ env: 'PARSE_SERVER_DATABASE_PROXY_PASSWORD',
+ help:
+ 'The MongoDB driver option to configure a Socks5 proxy password when the proxy requires username/password authentication.',
+ },
+ proxyPort: {
+ env: 'PARSE_SERVER_DATABASE_PROXY_PORT',
+ help:
+ 'The MongoDB driver option to configure a Socks5 proxy port used for creating TCP connections.',
+ action: parsers.numberParser('proxyPort'),
+ },
+ proxyUsername: {
+ env: 'PARSE_SERVER_DATABASE_PROXY_USERNAME',
+ help:
+ 'The MongoDB driver option to configure a Socks5 proxy username when the proxy requires username/password authentication.',
+ },
+ readConcernLevel: {
+ env: 'PARSE_SERVER_DATABASE_READ_CONCERN_LEVEL',
+ help: 'The MongoDB driver option to specify the level of isolation.',
+ },
+ readPreference: {
+ env: 'PARSE_SERVER_DATABASE_READ_PREFERENCE',
+ help: 'The MongoDB driver option to specify the read preferences for this connection.',
+ },
+ readPreferenceTags: {
+ env: 'PARSE_SERVER_DATABASE_READ_PREFERENCE_TAGS',
+ help:
+ 'The MongoDB driver option to specify the tags document as a comma-separated list of colon-separated key-value pairs.',
+ action: parsers.arrayParser,
+ },
+ replicaSet: {
+ env: 'PARSE_SERVER_DATABASE_REPLICA_SET',
+ help:
+ 'The MongoDB driver option to specify the name of the replica set, if the mongod is a member of a replica set.',
+ },
+ retryReads: {
+ env: 'PARSE_SERVER_DATABASE_RETRY_READS',
+ help: 'The MongoDB driver option to enable retryable reads.',
+ action: parsers.booleanParser,
+ },
retryWrites: {
env: 'PARSE_SERVER_DATABASE_RETRY_WRITES',
help: 'The MongoDB driver option to set whether to retry failed writes.',
@@ -1198,12 +1385,87 @@ module.exports.DatabaseOptions = {
'The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires.',
action: parsers.numberParser('schemaCacheTtl'),
},
+ serverMonitoringMode: {
+ env: 'PARSE_SERVER_DATABASE_SERVER_MONITORING_MODE',
+ help:
+ 'The MongoDB driver option to instruct the driver monitors to use a specific monitoring mode.',
+ },
+ serverSelectionTimeoutMS: {
+ env: 'PARSE_SERVER_DATABASE_SERVER_SELECTION_TIMEOUT_MS',
+ help:
+ '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.',
action: parsers.numberParser('socketTimeoutMS'),
},
+ srvMaxHosts: {
+ env: 'PARSE_SERVER_DATABASE_SRV_MAX_HOSTS',
+ help:
+ 'The MongoDB driver option to specify the maximum number of hosts to connect to when using an srv connection string, a setting of 0 means unlimited hosts.',
+ action: parsers.numberParser('srvMaxHosts'),
+ },
+ srvServiceName: {
+ env: 'PARSE_SERVER_DATABASE_SRV_SERVICE_NAME',
+ help: 'The MongoDB driver option to modify the srv URI service name.',
+ },
+ ssl: {
+ env: 'PARSE_SERVER_DATABASE_SSL',
+ help:
+ 'The MongoDB driver option to enable or disable TLS/SSL for the connection (equivalent to tls option).',
+ action: parsers.booleanParser,
+ },
+ tls: {
+ env: 'PARSE_SERVER_DATABASE_TLS',
+ help: 'The MongoDB driver option to enable or disable TLS/SSL for the connection.',
+ action: parsers.booleanParser,
+ },
+ tlsAllowInvalidCertificates: {
+ env: 'PARSE_SERVER_DATABASE_TLS_ALLOW_INVALID_CERTIFICATES',
+ help:
+ 'The MongoDB driver option to bypass validation of the certificates presented by the mongod/mongos instance.',
+ action: parsers.booleanParser,
+ },
+ tlsAllowInvalidHostnames: {
+ env: 'PARSE_SERVER_DATABASE_TLS_ALLOW_INVALID_HOSTNAMES',
+ help:
+ 'The MongoDB driver option to disable hostname validation of the certificate presented by the mongod/mongos instance.',
+ action: parsers.booleanParser,
+ },
+ tlsCAFile: {
+ env: 'PARSE_SERVER_DATABASE_TLS_CAFILE',
+ help:
+ 'The MongoDB driver option to specify the location of a local .pem file that contains the root certificate chain from the Certificate Authority.',
+ },
+ tlsCertificateKeyFile: {
+ env: 'PARSE_SERVER_DATABASE_TLS_CERTIFICATE_KEY_FILE',
+ help:
+ "The MongoDB driver option to specify the location of a local .pem file that contains the client's TLS/SSL certificate and key.",
+ },
+ tlsCertificateKeyFilePassword: {
+ env: 'PARSE_SERVER_DATABASE_TLS_CERTIFICATE_KEY_FILE_PASSWORD',
+ help: 'The MongoDB driver option to specify the password to decrypt the tlsCertificateKeyFile.',
+ },
+ tlsInsecure: {
+ env: 'PARSE_SERVER_DATABASE_TLS_INSECURE',
+ help: 'The MongoDB driver option to disable various certificate validations.',
+ action: parsers.booleanParser,
+ },
+ waitQueueTimeoutMS: {
+ env: 'PARSE_SERVER_DATABASE_WAIT_QUEUE_TIMEOUT_MS',
+ help:
+ 'The MongoDB driver option to specify the maximum time in milliseconds that a thread can wait for a connection to become available.',
+ action: parsers.numberParser('waitQueueTimeoutMS'),
+ },
+ zlibCompressionLevel: {
+ env: 'PARSE_SERVER_DATABASE_ZLIB_COMPRESSION_LEVEL',
+ help:
+ 'The MongoDB driver option to specify the compression level if using zlib for network compression (0-9).',
+ action: parsers.numberParser('zlibCompressionLevel'),
+ },
};
module.exports.AuthAdapter = {
enabled: {
@@ -1215,30 +1477,32 @@ module.exports.AuthAdapter = {
module.exports.LogLevels = {
cloudFunctionError: {
env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_ERROR',
- help: 'Log level used by the Cloud Code Functions on error. Default is `error`.',
+ help:
+ 'Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.',
default: 'error',
},
cloudFunctionSuccess: {
env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_SUCCESS',
- help: 'Log level used by the Cloud Code Functions on success. Default is `info`.',
+ help:
+ 'Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.',
default: 'info',
},
triggerAfter: {
env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER',
help:
- 'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`.',
+ 'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values.',
default: 'info',
},
triggerBeforeError: {
env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR',
help:
- 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`.',
+ 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.',
default: 'error',
},
triggerBeforeSuccess: {
env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS',
help:
- 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`.',
+ 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.',
default: 'info',
},
};
diff --git a/src/Options/docs.js b/src/Options/docs.js
index 4d268847b1..9569239ef7 100644
--- a/src/Options/docs.js
+++ b/src/Options/docs.js
@@ -45,6 +45,7 @@
* @property {Boolean} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.
* @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
* @property {Boolean} enableInsecureAuthAdapters Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.
+ * @property {Boolean} enableSanitizedErrorResponse If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`.
* @property {Boolean} encodeParseObjectInCloudFunction If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`.
ℹ️ The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.
* @property {String} encryptionKey Key for encrypting your files
* @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.
@@ -87,7 +88,7 @@
* @property {Boolean} preventLoginWithUnverifiedEmail Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required.
Default is `false`.
Requires option `verifyUserEmails: true`.
* @property {Boolean} preventSignupWithUnverifiedEmail If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.
Default is `false`.
Requires option `verifyUserEmails: true`.
* @property {ProtectedFields} protectedFields Protected fields that should be treated with extra security when fetching details.
- * @property {String} publicServerURL Public URL to your parse server with http:// or https://.
+ * @property {Union} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`.
* @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications
* @property {RateLimitOptions[]} rateLimit Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.
ℹ️ Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.
* @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes
@@ -122,7 +123,7 @@
* @property {String[]} requestMethods Optional, the HTTP request methods to which the rate limit should be applied, default is all methods.
* @property {String} requestPath The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expression. See: https://expressjs.com/en/guide/routing.html
* @property {Number} requestTimeWindow The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied.
- * @property {String} zone The type of rate limit to apply. The following types are supported:
- `global`: rate limit based on the number of requests made by all users
- `ip`: rate limit based on the IP address of the request
- `user`: rate limit based on the user ID of the request
- `session`: rate limit based on the session token of the request
:default: 'ip'
+ * @property {String} zone The type of rate limit to apply. The following types are supported:- `global`: rate limit based on the number of requests made by all users
- `ip`: rate limit based on the IP address of the request
- `user`: rate limit based on the user ID of the request
- `session`: rate limit based on the session token of the request
Default is `ip`.
*/
/**
@@ -238,10 +239,33 @@
* @property {String[]} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.
It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.
Defaults to `^(?![xXsS]?[hH][tT][mM][lL]?$)` which allows any file extension except those MIME types that are mapped to `text/html` and are rendered as website by a web browser.
*/
+/**
+ * @interface LogLevel
+ * @property {StringLiteral} debug Debug level
+ * @property {StringLiteral} error Error level - highest priority
+ * @property {StringLiteral} info Info level - default
+ * @property {StringLiteral} silly Silly level - lowest priority
+ * @property {StringLiteral} verbose Verbose level
+ * @property {StringLiteral} warn Warning level
+ */
+
+/**
+ * @interface LogClientEvent
+ * @property {String[]} keys Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged.
+ * @property {String} logLevel The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`.
+ * @property {String} name The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events.
+ */
+
/**
* @interface DatabaseOptions
+ * @property {Boolean} allowPublicExplain Set to `true` to allow `Parse.Query.explain` without master key.
⚠️ Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes.
+ * @property {String} appName The MongoDB driver option to specify the name of the application that created this MongoClient instance.
+ * @property {String} authMechanism The MongoDB driver option to specify the authentication mechanism that MongoDB will use to authenticate the connection.
+ * @property {Any} authMechanismProperties The MongoDB driver option to specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs.
+ * @property {String} authSource The MongoDB driver option to specify the database name associated with the user's credentials.
* @property {Boolean} autoSelectFamily The MongoDB driver option to set whether the socket attempts to connect to IPv6 and IPv4 addresses until a connection is established. If available, the driver will select the first IPv6 address.
* @property {Number} autoSelectFamilyAttemptTimeout The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead.
+ * @property {Union} compressors The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance.
* @property {Number} connectTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout.
* @property {Boolean} createIndexRoleName Set to `true` to automatically create a unique index on the name field of the _Role collection on server start. Set to `false` to skip index creation. Default is `true`.
⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.
* @property {Boolean} createIndexUserEmail Set to `true` to automatically create indexes on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.
⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.
@@ -250,15 +274,46 @@
* @property {Boolean} createIndexUserPasswordResetToken Set to `true` to automatically create an index on the _perishable_token field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.
⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.
* @property {Boolean} createIndexUserUsername Set to `true` to automatically create indexes on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.
⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.
* @property {Boolean} createIndexUserUsernameCaseInsensitive Set to `true` to automatically create a case-insensitive index on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.
⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.
+ * @property {Boolean} directConnection The MongoDB driver option to force a Single topology type with a connection string containing one host.
* @property {Boolean} disableIndexFieldValidation Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later.
* @property {Boolean} enableSchemaHooks Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required.
+ * @property {Boolean} forceServerObjectId The MongoDB driver option to force server to assign _id values instead of driver.
+ * @property {Number} heartbeatFrequencyMS The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment.
+ * @property {Boolean} loadBalanced The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service.
+ * @property {Number} localThresholdMS The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances.
+ * @property {LogClientEvent[]} logClientEvents An array of MongoDB client event configurations to enable logging of specific events.
+ * @property {Number} maxConnecting The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool.
+ * @property {Number} maxIdleTimeMS The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed.
* @property {Number} maxPoolSize The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver.
* @property {Number} maxStalenessSeconds The MongoDB driver option to set the maximum replication lag for reads from secondary nodes.
* @property {Number} maxTimeMS The MongoDB driver option to set a cumulative time limit in milliseconds for processing operations on a cursor.
* @property {Number} minPoolSize The MongoDB driver option to set the minimum number of opened, cached, ready-to-use database connections maintained by the driver.
+ * @property {String} proxyHost The MongoDB driver option to configure a Socks5 proxy host used for creating TCP connections.
+ * @property {String} proxyPassword The MongoDB driver option to configure a Socks5 proxy password when the proxy requires username/password authentication.
+ * @property {Number} proxyPort The MongoDB driver option to configure a Socks5 proxy port used for creating TCP connections.
+ * @property {String} proxyUsername The MongoDB driver option to configure a Socks5 proxy username when the proxy requires username/password authentication.
+ * @property {String} readConcernLevel The MongoDB driver option to specify the level of isolation.
+ * @property {String} readPreference The MongoDB driver option to specify the read preferences for this connection.
+ * @property {Any[]} readPreferenceTags The MongoDB driver option to specify the tags document as a comma-separated list of colon-separated key-value pairs.
+ * @property {String} replicaSet The MongoDB driver option to specify the name of the replica set, if the mongod is a member of a replica set.
+ * @property {Boolean} retryReads The MongoDB driver option to enable retryable reads.
* @property {Boolean} retryWrites The MongoDB driver option to set whether to retry failed writes.
* @property {Number} schemaCacheTtl The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires.
+ * @property {String} serverMonitoringMode The MongoDB driver option to instruct the driver monitors to use a specific monitoring mode.
+ * @property {Number} serverSelectionTimeoutMS The MongoDB driver option to specify the amount of time in milliseconds for a server to be considered suitable for selection.
* @property {Number} socketTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, spent attempting to send or receive on a socket before timing out. Specifying 0 means no timeout.
+ * @property {Number} srvMaxHosts The MongoDB driver option to specify the maximum number of hosts to connect to when using an srv connection string, a setting of 0 means unlimited hosts.
+ * @property {String} srvServiceName The MongoDB driver option to modify the srv URI service name.
+ * @property {Boolean} ssl The MongoDB driver option to enable or disable TLS/SSL for the connection (equivalent to tls option).
+ * @property {Boolean} tls The MongoDB driver option to enable or disable TLS/SSL for the connection.
+ * @property {Boolean} tlsAllowInvalidCertificates The MongoDB driver option to bypass validation of the certificates presented by the mongod/mongos instance.
+ * @property {Boolean} tlsAllowInvalidHostnames The MongoDB driver option to disable hostname validation of the certificate presented by the mongod/mongos instance.
+ * @property {String} tlsCAFile The MongoDB driver option to specify the location of a local .pem file that contains the root certificate chain from the Certificate Authority.
+ * @property {String} tlsCertificateKeyFile The MongoDB driver option to specify the location of a local .pem file that contains the client's TLS/SSL certificate and key.
+ * @property {String} tlsCertificateKeyFilePassword The MongoDB driver option to specify the password to decrypt the tlsCertificateKeyFile.
+ * @property {Boolean} tlsInsecure The MongoDB driver option to disable various certificate validations.
+ * @property {Number} waitQueueTimeoutMS The MongoDB driver option to specify the maximum time in milliseconds that a thread can wait for a connection to become available.
+ * @property {Number} zlibCompressionLevel The MongoDB driver option to specify the compression level if using zlib for network compression (0-9).
*/
/**
@@ -268,9 +323,9 @@
/**
* @interface LogLevels
- * @property {String} cloudFunctionError Log level used by the Cloud Code Functions on error. Default is `error`.
- * @property {String} cloudFunctionSuccess Log level used by the Cloud Code Functions on success. Default is `info`.
- * @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`.
- * @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`.
- * @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`.
+ * @property {String} cloudFunctionError Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.
+ * @property {String} cloudFunctionSuccess Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.
+ * @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values.
+ * @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.
+ * @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.
*/
diff --git a/src/Options/index.js b/src/Options/index.js
index d5317646ba..cdeb7cd846 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -226,9 +226,9 @@ export interface ParseServerOptions {
/* If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`.
ℹ️ The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.
:DEFAULT: true */
encodeParseObjectInCloudFunction: ?boolean;
- /* Public URL to your parse server with http:// or https://.
+ /* Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`.
:ENV: PARSE_PUBLIC_SERVER_URL */
- publicServerURL: ?string;
+ publicServerURL: ?(string | (() => string) | (() => Promise));
/* The options for pages such as password reset and email verification.
:DEFAULT: {} */
pages: ?PagesOptions;
@@ -347,6 +347,9 @@ export interface ParseServerOptions {
rateLimit: ?(RateLimitOptions[]);
/* Options to customize the request context using inversion of control/dependency injection.*/
requestContextMiddleware: ?(req: any, res: any, next: any) => void;
+ /* If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`.
+ :DEFAULT: true */
+ enableSanitizedErrorResponse: ?boolean;
}
export interface RateLimitOptions {
@@ -370,16 +373,15 @@ export interface RateLimitOptions {
/* Optional, the URL of the Redis server to store rate limit data. This allows to rate limit requests for multiple servers by calculating the sum of all requests across all servers. This is useful if multiple servers are processing requests behind a load balancer. For example, the limit of 10 requests is reached if each of 2 servers processed 5 requests.
*/
redisUrl: ?string;
- /*
- The type of rate limit to apply. The following types are supported:
-
- - `global`: rate limit based on the number of requests made by all users
- - `ip`: rate limit based on the IP address of the request
- - `user`: rate limit based on the user ID of the request
- - `session`: rate limit based on the session token of the request
-
- :default: 'ip'
- */
+ /* The type of rate limit to apply. The following types are supported:
+
+ - `global`: rate limit based on the number of requests made by all users
+ - `ip`: rate limit based on the IP address of the request
+ - `user`: rate limit based on the user ID of the request
+ - `session`: rate limit based on the session token of the request
+
+ Default is `ip`.
+ :DEFAULT: ip */
zone: ?string;
}
@@ -608,6 +610,32 @@ export interface FileUploadOptions {
enableForPublic: ?boolean;
}
+/* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'silly'` - Silly level (lowest priority) */
+export interface LogLevel {
+ /* Error level - highest priority */
+ error: 'error';
+ /* Warning level */
+ warn: 'warn';
+ /* Info level - default */
+ info: 'info';
+ /* Verbose level */
+ verbose: 'verbose';
+ /* Debug level */
+ debug: 'debug';
+ /* Silly level - lowest priority */
+ silly: 'silly';
+}
+
+export interface LogClientEvent {
+ /* The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. */
+ name: string;
+ /* Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. */
+ keys: ?(string[]);
+ /* The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`.
+ :DEFAULT: info */
+ logLevel: ?string;
+}
+
export interface DatabaseOptions {
/* Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required.
:DEFAULT: false */
@@ -624,6 +652,12 @@ export interface DatabaseOptions {
minPoolSize: ?number;
/* The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver. */
maxPoolSize: ?number;
+ /* The MongoDB driver option to specify the amount of time in milliseconds for a server to be considered suitable for selection. */
+ serverSelectionTimeoutMS: ?number;
+ /* The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed. */
+ maxIdleTimeMS: ?number;
+ /* The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment. */
+ heartbeatFrequencyMS: ?number;
/* The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout. */
connectTimeoutMS: ?number;
/* The MongoDB driver option to specify the amount of time, in milliseconds, spent attempting to send or receive on a socket before timing out. Specifying 0 means no timeout. */
@@ -632,6 +666,70 @@ export interface DatabaseOptions {
autoSelectFamily: ?boolean;
/* The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead. */
autoSelectFamilyAttemptTimeout: ?number;
+ /* The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool. */
+ maxConnecting: ?number;
+ /* The MongoDB driver option to specify the maximum time in milliseconds that a thread can wait for a connection to become available. */
+ waitQueueTimeoutMS: ?number;
+ /* The MongoDB driver option to specify the name of the replica set, if the mongod is a member of a replica set. */
+ replicaSet: ?string;
+ /* The MongoDB driver option to force a Single topology type with a connection string containing one host. */
+ directConnection: ?boolean;
+ /* The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service. */
+ loadBalanced: ?boolean;
+ /* The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances. */
+ localThresholdMS: ?number;
+ /* The MongoDB driver option to specify the maximum number of hosts to connect to when using an srv connection string, a setting of 0 means unlimited hosts. */
+ srvMaxHosts: ?number;
+ /* The MongoDB driver option to modify the srv URI service name. */
+ srvServiceName: ?string;
+ /* The MongoDB driver option to enable or disable TLS/SSL for the connection. */
+ tls: ?boolean;
+ /* The MongoDB driver option to enable or disable TLS/SSL for the connection (equivalent to tls option). */
+ ssl: ?boolean;
+ /* The MongoDB driver option to specify the location of a local .pem file that contains the client's TLS/SSL certificate and key. */
+ tlsCertificateKeyFile: ?string;
+ /* The MongoDB driver option to specify the password to decrypt the tlsCertificateKeyFile. */
+ tlsCertificateKeyFilePassword: ?string;
+ /* The MongoDB driver option to specify the location of a local .pem file that contains the root certificate chain from the Certificate Authority. */
+ tlsCAFile: ?string;
+ /* The MongoDB driver option to bypass validation of the certificates presented by the mongod/mongos instance. */
+ tlsAllowInvalidCertificates: ?boolean;
+ /* The MongoDB driver option to disable hostname validation of the certificate presented by the mongod/mongos instance. */
+ tlsAllowInvalidHostnames: ?boolean;
+ /* The MongoDB driver option to disable various certificate validations. */
+ tlsInsecure: ?boolean;
+ /* The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance. */
+ compressors: ?(string[] | string);
+ /* The MongoDB driver option to specify the compression level if using zlib for network compression (0-9). */
+ zlibCompressionLevel: ?number;
+ /* The MongoDB driver option to specify the read preferences for this connection. */
+ readPreference: ?string;
+ /* The MongoDB driver option to specify the tags document as a comma-separated list of colon-separated key-value pairs. */
+ readPreferenceTags: ?(any[]);
+ /* The MongoDB driver option to specify the level of isolation. */
+ readConcernLevel: ?string;
+ /* The MongoDB driver option to specify the database name associated with the user's credentials. */
+ authSource: ?string;
+ /* The MongoDB driver option to specify the authentication mechanism that MongoDB will use to authenticate the connection. */
+ authMechanism: ?string;
+ /* The MongoDB driver option to specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs. */
+ authMechanismProperties: ?any;
+ /* The MongoDB driver option to specify the name of the application that created this MongoClient instance. */
+ appName: ?string;
+ /* The MongoDB driver option to enable retryable reads. */
+ retryReads: ?boolean;
+ /* The MongoDB driver option to force server to assign _id values instead of driver. */
+ forceServerObjectId: ?boolean;
+ /* The MongoDB driver option to instruct the driver monitors to use a specific monitoring mode. */
+ serverMonitoringMode: ?string;
+ /* The MongoDB driver option to configure a Socks5 proxy host used for creating TCP connections. */
+ proxyHost: ?string;
+ /* The MongoDB driver option to configure a Socks5 proxy port used for creating TCP connections. */
+ proxyPort: ?number;
+ /* The MongoDB driver option to configure a Socks5 proxy username when the proxy requires username/password authentication. */
+ proxyUsername: ?string;
+ /* The MongoDB driver option to configure a Socks5 proxy password when the proxy requires username/password authentication. */
+ proxyPassword: ?string;
/* Set to `true` to automatically create indexes on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.
⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.
:DEFAULT: true */
createIndexUserEmail: ?boolean;
@@ -655,6 +753,11 @@ export interface DatabaseOptions {
createIndexRoleName: ?boolean;
/* Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. */
disableIndexFieldValidation: ?boolean;
+ /* Set to `true` to allow `Parse.Query.explain` without master key.
⚠️ Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes.
+ :DEFAULT: true */
+ allowPublicExplain: ?boolean;
+ /* An array of MongoDB client event configurations to enable logging of specific events. */
+ logClientEvents: ?(LogClientEvent[]);
}
export interface AuthAdapter {
@@ -666,23 +769,23 @@ export interface AuthAdapter {
}
export interface LogLevels {
- /* Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`.
+ /* Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values.
:DEFAULT: info
*/
triggerAfter: ?string;
- /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`.
+ /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.
:DEFAULT: info
*/
triggerBeforeSuccess: ?string;
- /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`.
+ /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.
:DEFAULT: error
*/
triggerBeforeError: ?string;
- /* Log level used by the Cloud Code Functions on success. Default is `info`.
+ /* Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.
:DEFAULT: info
*/
cloudFunctionSuccess: ?string;
- /* Log level used by the Cloud Code Functions on error. Default is `error`.
+ /* Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.
:DEFAULT: error
*/
cloudFunctionError: ?string;
diff --git a/src/Options/parsers.js b/src/Options/parsers.js
index 384b5494ef..3fdad89dc3 100644
--- a/src/Options/parsers.js
+++ b/src/Options/parsers.js
@@ -55,7 +55,7 @@ function moduleOrObjectParser(opt) {
}
try {
return JSON.parse(opt);
- } catch (e) {
+ } catch {
/* */
}
return opt;
diff --git a/src/RestQuery.js b/src/RestQuery.js
index dd226f249c..2064ffd0df 100644
--- a/src/RestQuery.js
+++ b/src/RestQuery.js
@@ -7,6 +7,7 @@ const triggers = require('./triggers');
const { continueWhile } = require('parse/lib/node/promiseUtils');
const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
const { enforceRoleSecurity } = require('./SharedRest');
+const { createSanitizedError } = require('./Error');
// restOptions can include:
// skip
@@ -51,7 +52,7 @@ async function RestQuery({
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad query type');
}
const isGet = method === RestQuery.Method.get;
- enforceRoleSecurity(method, className, auth);
+ enforceRoleSecurity(method, className, auth, config);
const result = runBeforeFind
? await triggers.maybeRunQueryTrigger(
triggers.Types.beforeFind,
@@ -120,7 +121,7 @@ function _UnsafeRestQuery(
if (!this.auth.isMaster) {
if (this.className == '_Session') {
if (!this.auth.user) {
- throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
+ throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
}
this.restWhere = {
$and: [
@@ -421,9 +422,10 @@ _UnsafeRestQuery.prototype.validateClientClassCreation = function () {
.then(schemaController => schemaController.hasClass(this.className))
.then(hasClass => {
if (hasClass !== true) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- 'This user is not allowed to access ' + 'non-existent class: ' + this.className
+ 'This user is not allowed to access ' + 'non-existent class: ' + this.className,
+ this.config
);
}
});
@@ -800,9 +802,10 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () {
) || [];
for (const key of protectedFields) {
if (this.restWhere[key]) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- `This user is not allowed to query ${key} on class ${this.className}`
+ `This user is not allowed to query ${key} on class ${this.className}`,
+ this.config
);
}
}
@@ -856,31 +859,54 @@ _UnsafeRestQuery.prototype.handleExcludeKeys = function () {
};
// Augments this.response with data at the paths provided in this.include.
-_UnsafeRestQuery.prototype.handleInclude = function () {
+_UnsafeRestQuery.prototype.handleInclude = async function () {
if (this.include.length == 0) {
return;
}
- var pathResponse = includePath(
- this.config,
- this.auth,
- this.response,
- this.include[0],
- this.context,
- this.restOptions
- );
- if (pathResponse.then) {
- return pathResponse.then(newResponse => {
- this.response = newResponse;
- this.include = this.include.slice(1);
- return this.handleInclude();
+ const indexedResults = this.response.results.reduce((indexed, result, i) => {
+ indexed[result.objectId] = i;
+ return indexed;
+ }, {});
+
+ // Build the execution tree
+ const executionTree = {}
+ this.include.forEach(path => {
+ let current = executionTree;
+ path.forEach((node) => {
+ if (!current[node]) {
+ current[node] = {
+ path,
+ children: {}
+ };
+ }
+ current = current[node].children
});
- } else if (this.include.length > 0) {
- this.include = this.include.slice(1);
- return this.handleInclude();
+ });
+
+ const recursiveExecutionTree = async (treeNode) => {
+ const { path, children } = treeNode;
+ const pathResponse = includePath(
+ this.config,
+ this.auth,
+ this.response,
+ path,
+ this.context,
+ this.restOptions,
+ this,
+ );
+ if (pathResponse.then) {
+ const newResponse = await pathResponse
+ newResponse.results.forEach(newObject => {
+ // We hydrate the root of each result with sub results
+ this.response.results[indexedResults[newObject.objectId]][path[0]] = newObject[path[0]];
+ })
+ }
+ return Promise.all(Object.values(children).map(recursiveExecutionTree));
}
- return pathResponse;
+ await Promise.all(Object.values(executionTree).map(recursiveExecutionTree));
+ this.include = []
};
//Returns a promise of a processed set of results
@@ -1018,7 +1044,6 @@ function includePath(config, auth, response, path, context, restOptions = {}) {
} else if (restOptions.readPreference) {
includeRestOptions.readPreference = restOptions.readPreference;
}
-
const queryPromises = Object.keys(pointersHash).map(async className => {
const objectIds = Array.from(pointersHash[className]);
let where;
@@ -1057,7 +1082,6 @@ function includePath(config, auth, response, path, context, restOptions = {}) {
}
return replace;
}, {});
-
var resp = {
results: replacePointers(response.results, path, replace),
};
diff --git a/src/RestWrite.js b/src/RestWrite.js
index 41b6c23468..a0de5577a5 100644
--- a/src/RestWrite.js
+++ b/src/RestWrite.js
@@ -17,6 +17,7 @@ import RestQuery from './RestQuery';
import _ from 'lodash';
import logger from './logger';
import { requiredColumns } from './Controllers/SchemaController';
+import { createSanitizedError } from './Error';
// query and data are both provided in REST API format. So data
// types are encoded by plain old objects.
@@ -29,9 +30,10 @@ import { requiredColumns } from './Controllers/SchemaController';
// for the _User class.
function RestWrite(config, auth, className, query, data, originalData, clientSDK, context, action) {
if (auth.isReadOnly) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- 'Cannot perform a write operation when using readOnlyMasterKey'
+ 'Cannot perform a write operation when using readOnlyMasterKey',
+ config
);
}
this.config = config;
@@ -199,9 +201,10 @@ RestWrite.prototype.validateClientClassCreation = function () {
.then(schemaController => schemaController.hasClass(this.className))
.then(hasClass => {
if (hasClass !== true) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- 'This user is not allowed to access ' + 'non-existent class: ' + this.className
+ 'This user is not allowed to access non-existent class: ' + this.className,
+ this.config
);
}
});
@@ -566,7 +569,6 @@ RestWrite.prototype.handleAuthData = async function (authData) {
// User found with provided authData
if (results.length === 1) {
-
this.storage.authProvider = Object.keys(authData).join(',');
const { hasMutatedAuthData, mutatedAuthData } = Auth.hasMutatedAuthData(
@@ -660,8 +662,11 @@ RestWrite.prototype.checkRestrictedFields = async function () {
}
if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) {
- const error = `Clients aren't allowed to manually update email verification.`;
- throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
+ throw createSanitizedError(
+ Parse.Error.OPERATION_FORBIDDEN,
+ "Clients aren't allowed to manually update email verification.",
+ this.config
+ );
}
};
@@ -1450,9 +1455,10 @@ RestWrite.prototype.runDatabaseOperation = function () {
}
if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.SESSION_MISSING,
- `Cannot modify user ${this.query.objectId}.`
+ `Cannot modify user ${this.query.objectId}.`,
+ this.config
);
}
diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js
index 8b6e447757..83db6dbab0 100644
--- a/src/Routers/ClassesRouter.js
+++ b/src/Routers/ClassesRouter.js
@@ -3,6 +3,7 @@ import rest from '../rest';
import _ from 'lodash';
import Parse from 'parse/node';
import { promiseEnsureIdempotency } from '../middlewares';
+import { createSanitizedError } from '../Error';
const ALLOWED_GET_QUERY_KEYS = [
'keys',
@@ -111,7 +112,7 @@ export class ClassesRouter extends PromiseRouter {
typeof req.body?.objectId === 'string' &&
req.body.objectId.startsWith('role:')
) {
- throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.');
+ throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.', req.config);
}
return rest.create(
req.config,
@@ -149,7 +150,7 @@ export class ClassesRouter extends PromiseRouter {
for (const [key, value] of _.entries(query)) {
try {
json[key] = JSON.parse(value);
- } catch (e) {
+ } catch {
json[key] = value;
}
}
diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js
index 5cb39abf47..f0bb483d7b 100644
--- a/src/Routers/FilesRouter.js
+++ b/src/Routers/FilesRouter.js
@@ -43,8 +43,7 @@ export class FilesRouter {
const config = Config.get(req.params.appId);
if (!config) {
res.status(403);
- const err = new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid application ID.');
- res.json({ code: err.code, error: err.message });
+ res.json({ code: Parse.Error.OPERATION_FORBIDDEN, error: 'Invalid application ID.' });
return;
}
@@ -310,7 +309,7 @@ export class FilesRouter {
const data = await filesController.getMetadata(filename);
res.status(200);
res.json(data);
- } catch (e) {
+ } catch {
res.status(200);
res.json({});
}
diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js
index 5a28b3bae1..6a05f7308f 100644
--- a/src/Routers/GlobalConfigRouter.js
+++ b/src/Routers/GlobalConfigRouter.js
@@ -3,6 +3,7 @@ import Parse from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
import * as triggers from '../triggers';
+import { createSanitizedError } from '../Error';
const getConfigFromParams = params => {
const config = new Parse.Config();
@@ -41,9 +42,10 @@ export class GlobalConfigRouter extends PromiseRouter {
async updateGlobalConfig(req) {
if (req.auth.isReadOnly) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- "read-only masterKey isn't allowed to update the config."
+ "read-only masterKey isn't allowed to update the config.",
+ req.config
);
}
const params = req.body.params || {};
diff --git a/src/Routers/GraphQLRouter.js b/src/Routers/GraphQLRouter.js
index d472ac9df5..67d7a24e46 100644
--- a/src/Routers/GraphQLRouter.js
+++ b/src/Routers/GraphQLRouter.js
@@ -1,6 +1,7 @@
import Parse from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
+import { createSanitizedError } from '../Error';
const GraphQLConfigPath = '/graphql-config';
@@ -14,9 +15,10 @@ export class GraphQLRouter extends PromiseRouter {
async updateGraphQLConfig(req) {
if (req.auth.isReadOnly) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- "read-only masterKey isn't allowed to update the GraphQL config."
+ "read-only masterKey isn't allowed to update the GraphQL config.",
+ req.config
);
}
const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {});
diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js
index 1ea3211684..74beec770c 100644
--- a/src/Routers/PagesRouter.js
+++ b/src/Routers/PagesRouter.js
@@ -432,7 +432,7 @@ export class PagesRouter extends PromiseRouter {
let data;
try {
data = await this.readFile(path);
- } catch (e) {
+ } catch {
return this.notFound();
}
@@ -474,7 +474,7 @@ export class PagesRouter extends PromiseRouter {
let data;
try {
data = await this.readFile(path);
- } catch (e) {
+ } catch {
return this.notFound();
}
@@ -517,7 +517,7 @@ export class PagesRouter extends PromiseRouter {
try {
const json = require(path.resolve('./', this.pagesConfig.localizationJsonPath));
this.jsonParameters = json;
- } catch (e) {
+ } catch {
throw errors.jsonFailedFileLoading;
}
}
diff --git a/src/Routers/PurgeRouter.js b/src/Routers/PurgeRouter.js
index 3195d134af..f346d64176 100644
--- a/src/Routers/PurgeRouter.js
+++ b/src/Routers/PurgeRouter.js
@@ -1,13 +1,15 @@
import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
import Parse from 'parse/node';
+import { createSanitizedError } from '../Error';
export class PurgeRouter extends PromiseRouter {
handlePurge(req) {
if (req.auth.isReadOnly) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- "read-only masterKey isn't allowed to purge a schema."
+ "read-only masterKey isn't allowed to purge a schema.",
+ req.config
);
}
return req.config.database
diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js
index 1c1c8f3b5f..696f19ed88 100644
--- a/src/Routers/PushRouter.js
+++ b/src/Routers/PushRouter.js
@@ -1,6 +1,7 @@
import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
import { Parse } from 'parse/node';
+import { createSanitizedError } from '../Error';
export class PushRouter extends PromiseRouter {
mountRoutes() {
@@ -9,9 +10,10 @@ export class PushRouter extends PromiseRouter {
static handlePOST(req) {
if (req.auth.isReadOnly) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- "read-only masterKey isn't allowed to send push notifications."
+ "read-only masterKey isn't allowed to send push notifications.",
+ req.config
);
}
const pushController = req.config.pushController;
diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js
index 0a42123af7..8713c95518 100644
--- a/src/Routers/SchemasRouter.js
+++ b/src/Routers/SchemasRouter.js
@@ -5,6 +5,7 @@ var Parse = require('parse/node').Parse,
import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
+import { createSanitizedError } from '../Error';
function classNameMismatchResponse(bodyClass, pathClass) {
throw new Parse.Error(
@@ -72,9 +73,10 @@ export const internalUpdateSchema = async (className, body, config) => {
async function createSchema(req) {
checkIfDefinedSchemasIsUsed(req);
if (req.auth.isReadOnly) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- "read-only masterKey isn't allowed to create a schema."
+ "read-only masterKey isn't allowed to create a schema.",
+ req.config
);
}
if (req.params.className && req.body?.className) {
@@ -94,9 +96,10 @@ async function createSchema(req) {
function modifySchema(req) {
checkIfDefinedSchemasIsUsed(req);
if (req.auth.isReadOnly) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- "read-only masterKey isn't allowed to update a schema."
+ "read-only masterKey isn't allowed to update a schema.",
+ req.config
);
}
if (req.body?.className && req.body.className != req.params.className) {
@@ -109,9 +112,10 @@ function modifySchema(req) {
const deleteSchema = req => {
if (req.auth.isReadOnly) {
- throw new Parse.Error(
+ throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
- "read-only masterKey isn't allowed to delete a schema."
+ "read-only masterKey isn't allowed to delete a schema.",
+ req.config
);
}
if (!SchemaController.classNameIsValid(req.params.className)) {
diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js
index 7668562965..3828e465e7 100644
--- a/src/Routers/UsersRouter.js
+++ b/src/Routers/UsersRouter.js
@@ -12,10 +12,12 @@ import {
Types as TriggerTypes,
getRequestObject,
resolveError,
+ inflate,
} from '../triggers';
import { promiseEnsureIdempotency } from '../middlewares';
import RestWrite from '../RestWrite';
import { logger } from '../logger';
+import { createSanitizedError } from '../Error';
export class UsersRouter extends ClassesRouter {
className() {
@@ -170,7 +172,7 @@ export class UsersRouter extends ClassesRouter {
handleMe(req) {
if (!req.info || !req.info.sessionToken) {
- throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
+ throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
}
const sessionToken = req.info.sessionToken;
return rest
@@ -185,7 +187,7 @@ export class UsersRouter extends ClassesRouter {
)
.then(response => {
if (!response.results || response.results.length == 0 || !response.results[0].user) {
- throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
+ throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
} else {
const user = response.results[0].user;
// Send token back on the login, because SDKs expect that.
@@ -333,7 +335,11 @@ export class UsersRouter extends ClassesRouter {
*/
async handleLogInAs(req) {
if (!req.auth.isMaster) {
- throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'master key is required');
+ throw createSanitizedError(
+ Parse.Error.OPERATION_FORBIDDEN,
+ 'master key is required',
+ req.config
+ );
}
const userId = req.body?.userId || req.query.userId;
@@ -418,7 +424,7 @@ export class UsersRouter extends ClassesRouter {
Config.validateEmailConfiguration({
emailAdapter: req.config.userController.adapter,
appName: req.config.appName,
- publicServerURL: req.config.publicServerURL,
+ publicServerURL: req.config.publicServerURL || req.config._publicServerURL,
emailVerifyTokenValidityDuration: req.config.emailVerifyTokenValidityDuration,
emailVerifyTokenReuseIfValid: req.config.emailVerifyTokenReuseIfValid,
});
@@ -444,21 +450,59 @@ export class UsersRouter extends ClassesRouter {
if (!email && !token) {
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
}
+
+ let userResults = null;
+ let userData = null;
+
+ // We can find the user using token
if (token) {
- const results = await req.config.database.find('_User', {
+ userResults = await req.config.database.find('_User', {
_perishable_token: token,
_perishable_token_expires_at: { $lt: Parse._encode(new Date()) },
});
- if (results && results[0] && results[0].email) {
- email = results[0].email;
+ if (userResults?.length > 0) {
+ userData = userResults[0];
+ if (userData.email) {
+ email = userData.email;
+ }
+ }
+ // Or using email if no token provided
+ } else if (typeof email === 'string') {
+ userResults = await req.config.database.find(
+ '_User',
+ { $or: [{ email }, { username: email, email: { $exists: false } }] },
+ { limit: 1 },
+ Auth.maintenance(req.config)
+ );
+ if (userResults?.length > 0) {
+ userData = userResults[0];
}
}
+
if (typeof email !== 'string') {
throw new Parse.Error(
Parse.Error.INVALID_EMAIL_ADDRESS,
'you must provide a valid email string'
);
}
+
+ if (userData) {
+ this._sanitizeAuthData(userData);
+ // Get files attached to user
+ await req.config.filesController.expandFilesInObject(req.config, userData);
+
+ const user = inflate('_User', userData);
+
+ await maybeRunTrigger(
+ TriggerTypes.beforePasswordResetRequest,
+ req.auth,
+ user,
+ null,
+ req.config,
+ req.info.context
+ );
+ }
+
const userController = req.config.userController;
try {
await userController.sendPasswordResetEmail(email);
diff --git a/src/Security/CheckGroups/CheckGroupServerConfig.js b/src/Security/CheckGroups/CheckGroupServerConfig.js
index 05a52a0275..ab2dfc4507 100644
--- a/src/Security/CheckGroups/CheckGroupServerConfig.js
+++ b/src/Security/CheckGroups/CheckGroupServerConfig.js
@@ -90,6 +90,21 @@ class CheckGroupServerConfig extends CheckGroup {
}
},
}),
+ new Check({
+ title: 'Public database explain disabled',
+ warning:
+ 'Database explain queries are publicly accessible, which may expose sensitive database performance information and schema details.',
+ solution:
+ "Change Parse Server configuration to 'databaseOptions.allowPublicExplain: false'. You will need to use master key to run explain queries.",
+ check: () => {
+ if (
+ config.databaseOptions?.allowPublicExplain === true ||
+ config.databaseOptions?.allowPublicExplain == null
+ ) {
+ throw 1;
+ }
+ },
+ }),
];
}
}
diff --git a/src/SharedRest.js b/src/SharedRest.js
index 0b4a07c320..3dc396d30c 100644
--- a/src/SharedRest.js
+++ b/src/SharedRest.js
@@ -6,12 +6,17 @@ const classesWithMasterOnlyAccess = [
'_JobSchedule',
'_Idempotency',
];
+const { createSanitizedError } = require('./Error');
+
// Disallowing access to the _Role collection except by master key
-function enforceRoleSecurity(method, className, auth) {
+function enforceRoleSecurity(method, className, auth, config) {
if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) {
if (method === 'delete' || method === 'find') {
- const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`;
- throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
+ throw createSanitizedError(
+ Parse.Error.OPERATION_FORBIDDEN,
+ `Clients aren't allowed to perform the ${method} operation on the installation collection.`,
+ config
+ );
}
}
@@ -21,14 +26,20 @@ function enforceRoleSecurity(method, className, auth) {
!auth.isMaster &&
!auth.isMaintenance
) {
- const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`;
- throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
+ throw createSanitizedError(
+ Parse.Error.OPERATION_FORBIDDEN,
+ `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`,
+ config
+ );
}
// readOnly masterKey is not allowed
if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) {
- const error = `read-only masterKey isn't allowed to perform the ${method} operation.`;
- throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
+ throw createSanitizedError(
+ Parse.Error.OPERATION_FORBIDDEN,
+ `read-only masterKey isn't allowed to perform the ${method} operation.`,
+ config
+ );
}
}
diff --git a/src/TestUtils.js b/src/TestUtils.js
index 2cd1493511..ec4cb29554 100644
--- a/src/TestUtils.js
+++ b/src/TestUtils.js
@@ -81,3 +81,4 @@ export class Connections {
return this.sockets.size;
}
}
+
diff --git a/src/Utils.js b/src/Utils.js
index 72b49aeeb2..0eca833552 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -82,7 +82,7 @@ class Utils {
try {
await fs.access(path);
return true;
- } catch (e) {
+ } catch {
return false;
}
}
@@ -410,6 +410,65 @@ class Utils {
'%' + char.charCodeAt(0).toString(16).toUpperCase()
);
}
+
+ /**
+ * Creates a JSON replacer function that handles Map, Set, and circular references.
+ * This replacer can be used with JSON.stringify to safely serialize complex objects.
+ *
+ * @returns {Function} A replacer function for JSON.stringify that:
+ * - Converts Map instances to plain objects
+ * - Converts Set instances to arrays
+ * - Replaces circular references with '[Circular]' marker
+ *
+ * @example
+ * const obj = { name: 'test', map: new Map([['key', 'value']]) };
+ * obj.self = obj; // circular reference
+ * JSON.stringify(obj, Utils.getCircularReplacer());
+ * // Output: {"name":"test","map":{"key":"value"},"self":"[Circular]"}
+ */
+ static getCircularReplacer() {
+ const seen = new WeakSet();
+ return (key, value) => {
+ if (value instanceof Map) {
+ return Object.fromEntries(value);
+ }
+ if (value instanceof Set) {
+ return Array.from(value);
+ }
+ if (typeof value === 'object' && value !== null) {
+ if (seen.has(value)) {
+ return '[Circular]';
+ }
+ seen.add(value);
+ }
+ return value;
+ };
+ }
+
+ /**
+ * Gets a nested property value from an object using dot notation.
+ * @param {Object} obj The object to get the property from.
+ * @param {String} path The property path in dot notation, e.g. 'databaseOptions.allowPublicExplain'.
+ * @returns {any} The property value or undefined if not found.
+ * @example
+ * const obj = { database: { options: { enabled: true } } };
+ * Utils.getNestedProperty(obj, 'database.options.enabled');
+ * // Output: true
+ */
+ static getNestedProperty(obj, path) {
+ if (!obj || !path) {
+ return undefined;
+ }
+ const keys = path.split('.');
+ let current = obj;
+ for (const key of keys) {
+ if (current == null || typeof current !== 'object') {
+ return undefined;
+ }
+ current = current[key];
+ }
+ return current;
+ }
}
module.exports = Utils;
diff --git a/src/batch.js b/src/batch.js
index 80fa028cc6..ca1bea621a 100644
--- a/src/batch.js
+++ b/src/batch.js
@@ -13,7 +13,7 @@ function mountOnto(router) {
function parseURL(urlString) {
try {
return new URL(urlString);
- } catch (error) {
+ } catch {
return undefined;
}
}
diff --git a/src/cli/utils/runner.js b/src/cli/utils/runner.js
index ed66cdfef8..6b18012af5 100644
--- a/src/cli/utils/runner.js
+++ b/src/cli/utils/runner.js
@@ -20,7 +20,7 @@ function logStartupOptions(options) {
if (typeof value === 'object') {
try {
value = JSON.stringify(value);
- } catch (e) {
+ } catch {
if (value && value.constructor && value.constructor.name) {
value = value.constructor.name;
}
diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js
index fa982de8f3..a057f8b3e6 100644
--- a/src/cloud-code/Parse.Cloud.js
+++ b/src/cloud-code/Parse.Cloud.js
@@ -349,6 +349,48 @@ ParseCloud.afterLogout = function (handler) {
triggers.addTrigger(triggers.Types.afterLogout, className, handler, Parse.applicationId);
};
+/**
+ * Registers the before password reset request function.
+ *
+ * **Available in Cloud Code only.**
+ *
+ * This function provides control in validating a password reset request
+ * before the reset email is sent. It is triggered after the user is found
+ * by email, but before the reset token is generated and the email is sent.
+ *
+ * Code example:
+ *
+ * ```
+ * Parse.Cloud.beforePasswordResetRequest(request => {
+ * if (request.object.get('banned')) {
+ * throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User is banned.');
+ * }
+ * });
+ * ```
+ *
+ * @method beforePasswordResetRequest
+ * @name Parse.Cloud.beforePasswordResetRequest
+ * @param {Function} func The function to run before a password reset request. This function can be async and should take one parameter a {@link Parse.Cloud.TriggerRequest};
+ */
+ParseCloud.beforePasswordResetRequest = function (handler, validationHandler) {
+ let className = '_User';
+ if (typeof handler === 'string' || isParseObjectConstructor(handler)) {
+ // validation will occur downstream, this is to maintain internal
+ // code consistency with the other hook types.
+ className = triggers.getClassName(handler);
+ handler = arguments[1];
+ validationHandler = arguments.length >= 2 ? arguments[2] : null;
+ }
+ triggers.addTrigger(triggers.Types.beforePasswordResetRequest, className, handler, Parse.applicationId);
+ if (validationHandler && validationHandler.rateLimit) {
+ addRateLimit(
+ { requestPath: `/requestPasswordReset`, requestMethods: 'POST', ...validationHandler.rateLimit },
+ Parse.applicationId,
+ true
+ );
+ }
+};
+
/**
* Registers an after save function.
*
diff --git a/src/defaults.js b/src/defaults.js
index a2b105d8db..07eeb51360 100644
--- a/src/defaults.js
+++ b/src/defaults.js
@@ -33,3 +33,21 @@ const computedDefaults = {
export default Object.assign({}, DefinitionDefaults, computedDefaults);
export const DefaultMongoURI = DefinitionDefaults.databaseURI;
+
+// Parse Server-specific database options that should be filtered out
+// before passing to MongoDB client
+export const ParseServerDatabaseOptions = [
+ 'allowPublicExplain',
+ 'createIndexRoleName',
+ 'createIndexUserEmail',
+ 'createIndexUserEmailCaseInsensitive',
+ 'createIndexUserEmailVerifyToken',
+ 'createIndexUserPasswordResetToken',
+ 'createIndexUserUsername',
+ 'createIndexUserUsernameCaseInsensitive',
+ 'disableIndexFieldValidation',
+ 'enableSchemaHooks',
+ 'logClientEvents',
+ 'maxTimeMS',
+ 'schemaCacheTtl',
+];
diff --git a/src/middlewares.js b/src/middlewares.js
index 6479987ba4..2fedce8f08 100644
--- a/src/middlewares.js
+++ b/src/middlewares.js
@@ -13,6 +13,7 @@ import { pathToRegexp } from 'path-to-regexp';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';
import { BlockList, isIPv4 } from 'net';
+import { createSanitizedHttpError } from './Error';
export const DEFAULT_ALLOWED_HEADERS =
'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, X-Parse-Request-Id, Content-Type, Pragma, Cache-Control';
@@ -79,7 +80,7 @@ export async function handleParseHeaders(req, res, next) {
if (Object.prototype.toString.call(context) !== '[object Object]') {
throw 'Context is not an object';
}
- } catch (e) {
+ } catch {
return malformedContext(req, res);
}
}
@@ -126,7 +127,7 @@ export async function handleParseHeaders(req, res, next) {
// to provide x-parse-app-id in header and parse a binary file will fail
try {
req.body = JSON.parse(req.body);
- } catch (e) {
+ } catch {
return invalidRequest(req, res);
}
fileViaJSON = true;
@@ -173,7 +174,7 @@ export async function handleParseHeaders(req, res, next) {
if (Object.prototype.toString.call(info.context) !== '[object Object]') {
throw 'Context is not an object';
}
- } catch (e) {
+ } catch {
return malformedContext(req, res);
}
}
@@ -213,6 +214,7 @@ export async function handleParseHeaders(req, res, next) {
});
return;
}
+ await config.loadKeys();
info.app = AppCache.get(info.appId);
req.config = config;
@@ -500,8 +502,9 @@ export function handleParseErrors(err, req, res, next) {
export function enforceMasterKeyAccess(req, res, next) {
if (!req.auth.isMaster) {
- res.status(403);
- res.end('{"error":"unauthorized: master key is required"}');
+ const error = createSanitizedHttpError(403, 'unauthorized: master key is required', req.config);
+ res.status(error.status);
+ res.end(`{"error":"${error.message}"}`);
return;
}
next();
@@ -509,10 +512,7 @@ export function enforceMasterKeyAccess(req, res, next) {
export function promiseEnforceMasterKeyAccess(request) {
if (!request.auth.isMaster) {
- const error = new Error();
- error.status = 403;
- error.message = 'unauthorized: master key is required';
- throw error;
+ throw createSanitizedHttpError(403, 'unauthorized: master key is required', request.config);
}
return Promise.resolve();
}
diff --git a/src/password.js b/src/password.js
index eebec14368..844f021937 100644
--- a/src/password.js
+++ b/src/password.js
@@ -8,7 +8,7 @@ try {
hash: _bcrypt.hash,
compare: _bcrypt.verify,
};
-} catch (e) {
+} catch {
/* */
}
diff --git a/src/request.js b/src/request.js
index bc58ee40ac..d5754d9201 100644
--- a/src/request.js
+++ b/src/request.js
@@ -31,7 +31,7 @@ class HTTPResponse {
if (!_data) {
try {
_data = JSON.parse(getText());
- } catch (e) {
+ } catch {
/* */
}
}
diff --git a/src/rest.js b/src/rest.js
index 8297121a68..66763715ea 100644
--- a/src/rest.js
+++ b/src/rest.js
@@ -13,6 +13,7 @@ var RestQuery = require('./RestQuery');
var RestWrite = require('./RestWrite');
var triggers = require('./triggers');
const { enforceRoleSecurity } = require('./SharedRest');
+const { createSanitizedError } = require('./Error');
function checkTriggers(className, config, types) {
return types.some(triggerType => {
@@ -35,6 +36,17 @@ async function runFindTriggers(
) {
const { isGet } = options;
+ if (restOptions && restOptions.explain && !auth.isMaster) {
+ const allowPublicExplain = config.databaseOptions?.allowPublicExplain ?? true;
+
+ if (!allowPublicExplain) {
+ throw new Parse.Error(
+ Parse.Error.INVALID_QUERY,
+ 'Using the explain query parameter requires the master key'
+ );
+ }
+ }
+
// Run beforeFind trigger - may modify query or return objects directly
const result = await triggers.maybeRunQueryTrigger(
triggers.Types.beforeFind,
@@ -123,7 +135,7 @@ async function runFindTriggers(
// Returns a promise for an object with optional keys 'results' and 'count'.
const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => {
- enforceRoleSecurity('find', className, auth);
+ enforceRoleSecurity('find', className, auth, config);
return runFindTriggers(
config,
auth,
@@ -138,7 +150,7 @@ const find = async (config, auth, className, restWhere, restOptions, clientSDK,
// get is just like find but only queries an objectId.
const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => {
- enforceRoleSecurity('get', className, auth);
+ enforceRoleSecurity('get', className, auth, config);
return runFindTriggers(
config,
auth,
@@ -161,7 +173,7 @@ function del(config, auth, className, objectId, context) {
throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth to delete user');
}
- enforceRoleSecurity('delete', className, auth);
+ enforceRoleSecurity('delete', className, auth, config);
let inflatedObject;
let schemaController;
@@ -184,7 +196,7 @@ function del(config, auth, className, objectId, context) {
firstResult.className = className;
if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) {
if (!auth.user || firstResult.user.objectId !== auth.user.id) {
- throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
+ throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
}
}
var cacheAdapter = config.cacheController;
@@ -246,13 +258,13 @@ function del(config, auth, className, objectId, context) {
);
})
.catch(error => {
- handleSessionMissingError(error, className, auth);
+ handleSessionMissingError(error, className, auth, config);
});
}
// Returns a promise for a {response, status, location} object.
function create(config, auth, className, restObject, clientSDK, context) {
- enforceRoleSecurity('create', className, auth);
+ enforceRoleSecurity('create', className, auth, config);
var write = new RestWrite(config, auth, className, null, restObject, null, clientSDK, context);
return write.execute();
}
@@ -261,7 +273,7 @@ function create(config, auth, className, restObject, clientSDK, context) {
// REST API is supposed to return.
// Usually, this is just updatedAt.
function update(config, auth, className, restWhere, restObject, clientSDK, context) {
- enforceRoleSecurity('update', className, auth);
+ enforceRoleSecurity('update', className, auth, config);
return Promise.resolve()
.then(async () => {
@@ -303,11 +315,11 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte
).execute();
})
.catch(error => {
- handleSessionMissingError(error, className, auth);
+ handleSessionMissingError(error, className, auth, config);
});
}
-function handleSessionMissingError(error, className, auth) {
+function handleSessionMissingError(error, className, auth, config) {
// If we're trying to update a user without / with bad session token
if (
className === '_User' &&
@@ -315,7 +327,7 @@ function handleSessionMissingError(error, className, auth) {
!auth.isMaster &&
!auth.isMaintenance
) {
- throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth.');
+ throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.', config);
}
throw error;
}
diff --git a/src/triggers.js b/src/triggers.js
index 26b107f062..e6abf20bb4 100644
--- a/src/triggers.js
+++ b/src/triggers.js
@@ -6,6 +6,7 @@ export const Types = {
beforeLogin: 'beforeLogin',
afterLogin: 'afterLogin',
afterLogout: 'afterLogout',
+ beforePasswordResetRequest: 'beforePasswordResetRequest',
beforeSave: 'beforeSave',
afterSave: 'afterSave',
beforeDelete: 'beforeDelete',
@@ -58,10 +59,10 @@ function validateClassNameForTriggers(className, type) {
// TODO: Allow proper documented way of using nested increment ops
throw 'Only afterSave is allowed on _PushStatus';
}
- if ((type === Types.beforeLogin || type === Types.afterLogin) && className !== '_User') {
+ if ((type === Types.beforeLogin || type === Types.afterLogin || type === Types.beforePasswordResetRequest) && className !== '_User') {
// TODO: check if upstream code will handle `Error` instance rather
// than this anti-pattern of throwing strings
- throw 'Only the _User class is allowed for the beforeLogin and afterLogin triggers';
+ throw 'Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers';
}
if (type === Types.afterLogout && className !== '_Session') {
// TODO: check if upstream code will handle `Error` instance rather
@@ -287,6 +288,7 @@ export function getRequestObject(
triggerType === Types.afterDelete ||
triggerType === Types.beforeLogin ||
triggerType === Types.afterLogin ||
+ triggerType === Types.beforePasswordResetRequest ||
triggerType === Types.afterFind
) {
// Set a copy of the context on the request object.
diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts
index 7a572a2f10..ad11050648 100644
--- a/types/Options/index.d.ts
+++ b/types/Options/index.d.ts
@@ -85,7 +85,7 @@ export interface ParseServerOptions {
cacheAdapter?: Adapter;
emailAdapter?: Adapter;
encodeParseObjectInCloudFunction?: boolean;
- publicServerURL?: string;
+ publicServerURL?: string | (() => string) | (() => Promise);
pages?: PagesOptions;
customPages?: CustomPagesOptions;
liveQuery?: LiveQueryOptions;
@@ -227,17 +227,66 @@ export interface FileUploadOptions {
enableForPublic?: boolean;
}
export interface DatabaseOptions {
+ // Parse Server custom options
+ allowPublicExplain?: boolean;
+ createIndexRoleName?: boolean;
+ createIndexUserEmail?: boolean;
+ createIndexUserEmailCaseInsensitive?: boolean;
+ createIndexUserEmailVerifyToken?: boolean;
+ createIndexUserPasswordResetToken?: boolean;
+ createIndexUserUsername?: boolean;
+ createIndexUserUsernameCaseInsensitive?: boolean;
+ disableIndexFieldValidation?: boolean;
enableSchemaHooks?: boolean;
- schemaCacheTtl?: number;
- retryWrites?: boolean;
+ logClientEvents?: any[];
+ // maxTimeMS is a MongoDB option but Parse Server applies it per-operation, not as a global client option
maxTimeMS?: number;
+ schemaCacheTtl?: number;
+
+ // MongoDB driver options
+ appName?: string;
+ authMechanism?: string;
+ authMechanismProperties?: any;
+ authSource?: string;
+ autoSelectFamily?: boolean;
+ autoSelectFamilyAttemptTimeout?: number;
+ compressors?: string[] | string;
+ connectTimeoutMS?: number;
+ directConnection?: boolean;
+ forceServerObjectId?: boolean;
+ heartbeatFrequencyMS?: number;
+ loadBalanced?: boolean;
+ localThresholdMS?: number;
+ maxConnecting?: number;
+ maxIdleTimeMS?: number;
+ maxPoolSize?: number;
maxStalenessSeconds?: number;
minPoolSize?: number;
- maxPoolSize?: number;
- connectTimeoutMS?: number;
+ proxyHost?: string;
+ proxyPassword?: string;
+ proxyPort?: number;
+ proxyUsername?: string;
+ readConcernLevel?: string;
+ readPreference?: string;
+ readPreferenceTags?: any[];
+ replicaSet?: string;
+ retryReads?: boolean;
+ retryWrites?: boolean;
+ serverMonitoringMode?: string;
+ serverSelectionTimeoutMS?: number;
socketTimeoutMS?: number;
- autoSelectFamily?: boolean;
- autoSelectFamilyAttemptTimeout?: number;
+ srvMaxHosts?: number;
+ srvServiceName?: string;
+ ssl?: boolean;
+ tls?: boolean;
+ tlsAllowInvalidCertificates?: boolean;
+ tlsAllowInvalidHostnames?: boolean;
+ tlsCAFile?: string;
+ tlsCertificateKeyFile?: string;
+ tlsCertificateKeyFilePassword?: string;
+ tlsInsecure?: boolean;
+ waitQueueTimeoutMS?: number;
+ zlibCompressionLevel?: number;
}
export interface AuthAdapter {
enabled?: boolean;
diff --git a/types/ParseServer.d.ts b/types/ParseServer.d.ts
index e504e03114..9570f0cf16 100644
--- a/types/ParseServer.d.ts
+++ b/types/ParseServer.d.ts
@@ -26,6 +26,11 @@ declare class ParseServer {
* @returns {Promise} a promise that resolves when the server is stopped
*/
handleShutdown(): Promise;
+ /**
+ * @static
+ * Allow developers to customize each request with inversion of control/dependency injection
+ */
+ static applyRequestContextMiddleware(api: any, options: any): void;
/**
* @static
* Create an express app for the parse server