From 2b87ddb5ac5fbd00a8b7c386bce0a8f4f413417f Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Tue, 26 May 2026 09:10:02 +0200 Subject: [PATCH 1/6] fix: preserve UTF-8 GraphQL upload filenames with latest graphql-upload Load the latest ESM-only graphql-upload through a small compatibility layer so GraphQL uploads keep accented filenames intact without changing Parse Server's public module model. Co-authored-by: Cursor --- package-lock.json | 211 ++++++++++----------- package.json | 2 +- spec/ParseGraphQLServer.spec.js | 65 +++++++ src/Adapters/Files/FilesAdapter.js | 2 +- src/GraphQL/ParseGraphQLSchema.js | 2 +- src/GraphQL/ParseGraphQLServer.js | 4 +- src/GraphQL/helpers/graphqlUpload.js | 51 +++++ src/GraphQL/loaders/defaultGraphQLTypes.js | 40 ++-- src/GraphQL/loaders/filesMutations.js | 15 +- 9 files changed, 254 insertions(+), 138 deletions(-) create mode 100644 src/GraphQL/helpers/graphqlUpload.js diff --git a/package-lock.json b/package-lock.json index 6bdbe3072d..dd0a1ffd60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "graphql": "16.13.2", "graphql-list-fields": "2.0.4", "graphql-relay": "0.10.2", - "graphql-upload": "15.0.2", + "graphql-upload": "17.0.0", "intersect": "1.0.1", "jsonwebtoken": "9.0.3", "jwks-rsa": "3.2.0", @@ -162,6 +162,7 @@ "integrity": "sha512-YM9lQpm0VfVco4DSyKooHS/fDTiKQcCHfxr7i3iL6a0kP/jNO5+4NFK6vtRDxaYisd5BrwOZHLJpPBnvRVpKPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@wry/caches": "^1.0.0", @@ -519,6 +520,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -3817,6 +3819,7 @@ "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", @@ -4313,6 +4316,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.11.0.tgz", "integrity": "sha512-GHoprlNQD51Xq2Ztd94HHV94MdFZQ3CVrpA04Fz8MVoHM0B7SlbmPEVIjwTbcv58z8QyjnrOuikS0rWF03k5dQ==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -4392,6 +4396,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", "dev": true, + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -5079,6 +5084,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", "dev": true, + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -5198,6 +5204,7 @@ "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.12.tgz", "integrity": "sha512-0mhiCR/4sZb00RVFJIUlMuiBkW3NMpVIW2Gse7noqEMoFGkvfPPAImEQbkBV8xga4KOPP4FdTRYuLLy32R1fPw==", "dev": true, + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^11.0.0", "@semantic-release/error": "^4.0.0", @@ -7586,6 +7593,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7983,6 +7991,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -8044,6 +8053,7 @@ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.1.tgz", "integrity": "sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==", "dev": true, + "peer": true, "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -8177,6 +8187,7 @@ "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", @@ -8274,6 +8285,7 @@ "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", @@ -9098,6 +9110,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9477,8 +9490,7 @@ "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/backoff": { "version": "2.5.0", @@ -9668,6 +9680,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -11794,6 +11807,7 @@ "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -12172,8 +12186,7 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/execa": { "version": "5.1.1", @@ -12225,6 +12238,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -12268,6 +12282,7 @@ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", "license": "MIT", + "peer": true, "dependencies": { "ip-address": "10.1.0" }, @@ -12981,11 +12996,12 @@ ] }, "node_modules/fs-capacitor": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-6.2.0.tgz", - "integrity": "sha512-nKcE1UduoSKX27NSZlg879LdQc94OtbOsEmKMN2MBNudXREvijRKx2GEBsTMTfws+BrbkJoEuynbGSVRSpauvw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-8.0.0.tgz", + "integrity": "sha512-+Lk6iSKajdGw+7XYxUkwIzreJ2G1JFlYOdnKJv5PzwFLVsoJYBpCuS7WPIUSNT1IbQaEWT1nhYU63Ud03DyzLA==", + "license": "MIT", "engines": { - "node": ">=10" + "node": "^14.17.0 || >=16.0.0" } }, "node_modules/fs-constants": { @@ -13200,7 +13216,6 @@ "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", @@ -13216,7 +13231,6 @@ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -13227,7 +13241,6 @@ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" @@ -13245,7 +13258,6 @@ "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -13263,7 +13275,6 @@ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -13285,7 +13296,6 @@ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -13302,7 +13312,6 @@ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "license": "BlueOak-1.0.0", "optional": true, - "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -13313,7 +13322,6 @@ "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "glob": "^10.3.7" }, @@ -13330,7 +13338,6 @@ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=14" }, @@ -13830,6 +13837,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -13866,26 +13874,27 @@ } }, "node_modules/graphql-upload": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-15.0.2.tgz", - "integrity": "sha512-ufJAkZJBKWRDD/4wJR3VZMy9QWTwqIYIciPtCEF5fCNgWF+V1p7uIgz+bP2YYLiS4OJBhCKR8rnqE/Wg3XPUiw==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-17.0.0.tgz", + "integrity": "sha512-AI42S1UR1mdqg+LQ7KqGbrgcf4l9gpPu/R0drM4vSA5C94NfIjYyCeCdpktEledvZoAL8JURLLeB53++WACo1w==", + "license": "MIT", "dependencies": { "@types/busboy": "^1.5.0", "@types/node": "*", "@types/object-path": "^0.11.1", "busboy": "^1.6.0", - "fs-capacitor": "^6.2.0", + "fs-capacitor": "^8.0.0", "http-errors": "^2.0.0", "object-path": "^0.11.8" }, "engines": { - "node": "^14.17.0 || ^16.0.0 || >= 18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=22.0.0" }, "funding": { "url": "https://github.com/sponsors/jaydenseric" }, "peerDependencies": { - "@types/express": "^4.0.29", + "@types/express": "4.0.29 - 5", "@types/koa": "^2.11.4", "graphql": "^16.3.0" }, @@ -14791,8 +14800,7 @@ "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/jackspeak": { "version": "3.4.3", @@ -14971,6 +14979,7 @@ "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@babel/parser": "^7.20.15", "@jsdoc/salty": "^0.2.1", @@ -16139,6 +16148,7 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, + "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -16166,6 +16176,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -16647,7 +16658,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "debug": "4" }, @@ -16718,7 +16728,6 @@ "dev": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", @@ -16729,22 +16738,6 @@ "node": ">=12" } }, - "node_modules/mongodb-runner/node_modules/gcp-metadata": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", - "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/mongodb-runner/node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -16752,7 +16745,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -16814,7 +16806,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -16836,8 +16827,7 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/mongodb-runner/node_modules/webidl-conversions": { "version": "3.0.1", @@ -16845,8 +16835,7 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true, "license": "BSD-2-Clause", - "optional": true, - "peer": true + "optional": true }, "node_modules/mongodb-runner/node_modules/whatwg-url": { "version": "5.0.0", @@ -16855,7 +16844,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -20512,6 +20500,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", @@ -20552,7 +20541,6 @@ "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.17.0.tgz", "integrity": "sha512-2Uio3Xfl5ldwJfls+RgGL+YbPcKQncWACWjYQFqlamvHZ4HJFjZhhZBbqd7jQ2LIkZYSvU90bm2dNW0rno+QFQ==", "license": "MIT", - "peer": true, "peerDependencies": { "pg": "^8" } @@ -20822,6 +20810,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", @@ -21827,6 +21816,7 @@ "integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -24015,6 +24005,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -25120,7 +25111,6 @@ "deprecated": "The `subscriptions-transport-ws` package is no longer maintained. We recommend you use `graphql-ws` instead. For help migrating Apollo software to `graphql-ws`, see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws For general help using `graphql-ws`, see https://github.com/enisdenjo/graphql-ws/blob/master/README.md", "dev": true, "optional": true, - "peer": true, "dependencies": { "backo2": "^1.0.2", "eventemitter3": "^3.1.0", @@ -25138,7 +25128,6 @@ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", "dev": true, "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -25150,7 +25139,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=8.3.0" }, @@ -25640,6 +25628,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -25894,6 +25883,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -26944,6 +26934,7 @@ "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.13.8.tgz", "integrity": "sha512-YM9lQpm0VfVco4DSyKooHS/fDTiKQcCHfxr7i3iL6a0kP/jNO5+4NFK6vtRDxaYisd5BrwOZHLJpPBnvRVpKPg==", "dev": true, + "peer": true, "requires": { "@graphql-typed-document-node/core": "^3.1.1", "@wry/caches": "^1.0.0", @@ -27175,6 +27166,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, + "peer": true, "requires": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -29419,6 +29411,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz", "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", "dev": true, + "peer": true, "requires": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", @@ -29819,6 +29812,7 @@ "version": "5.11.0", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.11.0.tgz", "integrity": "sha512-GHoprlNQD51Xq2Ztd94HHV94MdFZQ3CVrpA04Fz8MVoHM0B7SlbmPEVIjwTbcv58z8QyjnrOuikS0rWF03k5dQ==", + "peer": true, "requires": { "cluster-key-slot": "1.1.2" } @@ -29866,6 +29860,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", "dev": true, + "peer": true, "requires": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -30356,7 +30351,8 @@ "version": "9.1.6", "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", - "dev": true + "dev": true, + "peer": true }, "marked-terminal": { "version": "6.2.0", @@ -30425,6 +30421,7 @@ "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.12.tgz", "integrity": "sha512-0mhiCR/4sZb00RVFJIUlMuiBkW3NMpVIW2Gse7noqEMoFGkvfPPAImEQbkBV8xga4KOPP4FdTRYuLLy32R1fPw==", "dev": true, + "peer": true, "requires": { "@semantic-release/commit-analyzer": "^11.0.0", "@semantic-release/error": "^4.0.0", @@ -31986,7 +31983,8 @@ "picomatch": { "version": "4.0.3", "bundled": true, - "dev": true + "dev": true, + "peer": true } } }, @@ -32243,6 +32241,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "peer": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -32302,6 +32301,7 @@ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.1.tgz", "integrity": "sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==", "dev": true, + "peer": true, "requires": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -32430,6 +32430,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", "dev": true, + "peer": true, "requires": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", @@ -32483,6 +32484,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, + "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", @@ -32994,7 +32996,8 @@ "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -33277,8 +33280,7 @@ "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "backoff": { "version": "2.5.0", @@ -33409,6 +33411,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, + "peer": true, "requires": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -34903,6 +34906,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -35156,8 +35160,7 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "execa": { "version": "5.1.1", @@ -35197,6 +35200,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "peer": true, "requires": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -35247,6 +35251,7 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "peer": true, "requires": { "ip-address": "10.1.0" } @@ -35703,9 +35708,9 @@ "dev": true }, "fs-capacitor": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-6.2.0.tgz", - "integrity": "sha512-nKcE1UduoSKX27NSZlg879LdQc94OtbOsEmKMN2MBNudXREvijRKx2GEBsTMTfws+BrbkJoEuynbGSVRSpauvw==" + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-8.0.0.tgz", + "integrity": "sha512-+Lk6iSKajdGw+7XYxUkwIzreJ2G1JFlYOdnKJv5PzwFLVsoJYBpCuS7WPIUSNT1IbQaEWT1nhYU63Ud03DyzLA==" }, "fs-constants": { "version": "1.0.0", @@ -35859,7 +35864,6 @@ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", "optional": true, - "peer": true, "requires": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", @@ -35871,7 +35875,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "optional": true, - "peer": true, "requires": { "balanced-match": "^1.0.0" } @@ -35881,7 +35884,6 @@ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "optional": true, - "peer": true, "requires": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" @@ -35892,7 +35894,6 @@ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", "optional": true, - "peer": true, "requires": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -35905,7 +35906,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "optional": true, - "peer": true, "requires": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -35920,7 +35920,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "optional": true, - "peer": true, "requires": { "brace-expansion": "^2.0.1" } @@ -35929,15 +35928,13 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "optional": true, - "peer": true + "optional": true }, "rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "optional": true, - "peer": true, "requires": { "glob": "^10.3.7" } @@ -35946,8 +35943,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "optional": true, - "peer": true + "optional": true } } }, @@ -36308,7 +36304,8 @@ "graphql": { "version": "16.13.2", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", - "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==" + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", + "peer": true }, "graphql-list-fields": { "version": "2.0.4", @@ -36331,15 +36328,15 @@ } }, "graphql-upload": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-15.0.2.tgz", - "integrity": "sha512-ufJAkZJBKWRDD/4wJR3VZMy9QWTwqIYIciPtCEF5fCNgWF+V1p7uIgz+bP2YYLiS4OJBhCKR8rnqE/Wg3XPUiw==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-17.0.0.tgz", + "integrity": "sha512-AI42S1UR1mdqg+LQ7KqGbrgcf4l9gpPu/R0drM4vSA5C94NfIjYyCeCdpktEledvZoAL8JURLLeB53++WACo1w==", "requires": { "@types/busboy": "^1.5.0", "@types/node": "*", "@types/object-path": "^0.11.1", "busboy": "^1.6.0", - "fs-capacitor": "^6.2.0", + "fs-capacitor": "^8.0.0", "http-errors": "^2.0.0", "object-path": "^0.11.8" } @@ -36967,8 +36964,7 @@ "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "jackspeak": { "version": "3.4.3", @@ -37102,6 +37098,7 @@ "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", "dev": true, + "peer": true, "requires": { "@babel/parser": "^7.20.15", "@jsdoc/salty": "^0.2.1", @@ -37971,6 +37968,7 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, + "peer": true, "requires": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -37991,7 +37989,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", - "dev": true + "dev": true, + "peer": true }, "marked-terminal": { "version": "7.3.0", @@ -38311,7 +38310,6 @@ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "optional": true, - "peer": true, "requires": { "debug": "4" } @@ -38358,12 +38356,10 @@ "dev": true }, "gaxios": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "version": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", "dev": true, "optional": true, - "peer": true, "requires": { "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", @@ -38371,25 +38367,12 @@ "node-fetch": "^2.6.9" } }, - "gcp-metadata": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", - "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - } - }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "optional": true, - "peer": true, "requires": { "agent-base": "6", "debug": "4" @@ -38412,7 +38395,6 @@ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "optional": true, - "peer": true, "requires": { "whatwg-url": "^5.0.0" } @@ -38422,16 +38404,14 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "whatwg-url": { "version": "5.0.0", @@ -38439,7 +38419,6 @@ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, "optional": true, - "peer": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -40927,6 +40906,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", + "peer": true, "requires": { "pg-cloudflare": "^1.3.0", "pg-connection-string": "^2.11.0", @@ -40951,7 +40931,6 @@ "version": "2.17.0", "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.17.0.tgz", "integrity": "sha512-2Uio3Xfl5ldwJfls+RgGL+YbPcKQncWACWjYQFqlamvHZ4HJFjZhhZBbqd7jQ2LIkZYSvU90bm2dNW0rno+QFQ==", - "peer": true, "requires": {} }, "pg-int8": { @@ -41131,6 +41110,7 @@ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, + "peer": true, "requires": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", @@ -41841,6 +41821,7 @@ "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-25.0.3.tgz", "integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==", "dev": true, + "peer": true, "requires": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -43249,7 +43230,8 @@ "picomatch": { "version": "4.0.3", "bundled": true, - "dev": true + "dev": true, + "peer": true } } }, @@ -44024,7 +44006,6 @@ "integrity": "sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==", "dev": true, "optional": true, - "peer": true, "requires": { "backo2": "^1.0.2", "eventemitter3": "^3.1.0", @@ -44038,8 +44019,7 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "ws": { "version": "7.5.10", @@ -44047,7 +44027,6 @@ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "optional": true, - "peer": true, "requires": {} } } @@ -44379,7 +44358,8 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true + "dev": true, + "peer": true } } }, @@ -44559,7 +44539,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true + "dev": true, + "peer": true }, "typescript-eslint": { "version": "8.58.0", diff --git a/package.json b/package.json index fb164dcf58..a05f4a2725 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "graphql": "16.13.2", "graphql-list-fields": "2.0.4", "graphql-relay": "0.10.2", - "graphql-upload": "15.0.2", + "graphql-upload": "17.0.0", "intersect": "1.0.1", "jsonwebtoken": "9.0.3", "jwks-rsa": "3.2.0", diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 634bcf3d23..0d5aadac7d 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -7465,6 +7465,71 @@ describe('ParseGraphQLServer', () => { expect(res.status).toEqual(200); expect(await res.text()).toEqual('My File Content'); }); + + it('should preserve accented characters in uploaded filenames', async () => { + const clientMutationId = uuidv4(); + + parseServer = await global.reconfigureServer({ + publicServerURL: 'http://localhost:13377/parse', + }); + await createGQLFromParseServer(parseServer); + const body = new FormData(); + body.append( + 'operations', + JSON.stringify({ + query: ` + mutation CreateFile($input: CreateFileInput!) { + createFile(input: $input) { + clientMutationId + fileInfo { + name + url + } + } + } + `, + variables: { + input: { + clientMutationId, + upload: null, + }, + }, + }) + ); + body.append('map', JSON.stringify({ 1: ['variables.input.upload'] })); + body.append('1', 'My File Content', { + filename: 'café.txt', + contentType: 'text/plain', + }); + + let res = await fetch('http://localhost:13377/graphql', { + method: 'POST', + headers, + body, + }); + + expect(res.status).toEqual(200); + + const result = JSON.parse(await res.text()); + + expect(result.errors).toBeUndefined(); + expect(result.data?.createFile).not.toBeNull(); + if (result.errors || !result.data?.createFile) { + return; + } + expect(result.data.createFile.clientMutationId).toEqual(clientMutationId); + expect(result.data.createFile.fileInfo.name).toEqual( + jasmine.stringMatching(/_café.txt$/) + ); + expect(result.data.createFile.fileInfo.url).toEqual( + jasmine.stringMatching(/_caf%C3%A9.txt$/) + ); + + res = await fetch(result.data.createFile.fileInfo.url); + + expect(res.status).toEqual(200); + expect(await res.text()).toEqual('My File Content'); + }); }); }); diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js index 57f816521b..af839dd937 100644 --- a/src/Adapters/Files/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -112,7 +112,7 @@ export function validateFilename(filename): ?Parse.Error { return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.'); } - const regx = /^[_a-zA-Z0-9][a-zA-Z0-9@. ~_-]*$/; + const regx = /^[_\p{L}\p{N}][\p{L}\p{M}\p{N}@. ~_-]*$/u; if (!filename.match(regx)) { return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename contains invalid characters.'); } diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 5ecdd78de5..6191b732f9 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -130,7 +130,7 @@ class ParseGraphQLSchema { this.graphQLSchemaDirectives = {}; this.relayNodeInterface = null; - defaultGraphQLTypes.load(this); + await defaultGraphQLTypes.load(this); defaultRelaySchema.load(this); schemaTypes.load(this); diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 9d18f87d94..c8a3734707 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -1,4 +1,3 @@ -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js'; import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@as-integrations/express5'; import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled'; @@ -10,6 +9,7 @@ import defaultLogger from '../logger'; import { ParseGraphQLSchema } from './ParseGraphQLSchema'; import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/ParseGraphQLController'; import { createComplexityValidationPlugin } from './helpers/queryComplexity'; +import { createGraphQLUploadMiddleware } from './helpers/graphqlUpload'; const hasTypeIntrospection = (query) => { @@ -236,7 +236,7 @@ class ParseGraphQLServer { app.use(this.config.graphQLPath, handleParseErrors); app.use( this.config.graphQLPath, - graphqlUploadExpress({ + createGraphQLUploadMiddleware({ maxFileSize: this._transformMaxUploadSizeToBytes( this.parseServer.config.maxUploadSize || '20mb' ), diff --git a/src/GraphQL/helpers/graphqlUpload.js b/src/GraphQL/helpers/graphqlUpload.js new file mode 100644 index 0000000000..2cfb36d750 --- /dev/null +++ b/src/GraphQL/helpers/graphqlUpload.js @@ -0,0 +1,51 @@ +// Cache the dynamic imports so the ESM-only graphql-upload modules are +// resolved once and then reused by both schema loading and request handling. +let graphqlUploadModulesPromise; + +const loadGraphQLUploadModules = async () => { + if (!graphqlUploadModulesPromise) { + graphqlUploadModulesPromise = Promise.all([ + import('graphql-upload/GraphQLUpload.mjs'), + import('graphql-upload/processRequest.mjs'), + ]).then(([{ default: GraphQLUpload }, { default: processRequest }]) => ({ + GraphQLUpload, + processRequest, + })); + } + + return graphqlUploadModulesPromise; +}; + +// Expose the Upload scalar lazily so the rest of Parse Server can stay on the +// current module system while graphql-upload is now ESM-only. +const getGraphQLUpload = async () => { + const { GraphQLUpload } = await loadGraphQLUploadModules(); + return GraphQLUpload; +}; + +const createGraphQLUploadMiddleware = options => { + const uploadOptions = { + ...options, + // Decode multipart filename parameters as UTF-8 so filenames like + // "cafe.txt" with accents don't arrive as mojibake. + defParamCharset: 'utf8', + }; + + return async (req, res, next) => { + if (!req.is || !req.is('multipart/form-data')) { + return next(); + } + + try { + const { processRequest } = await loadGraphQLUploadModules(); + // graphql-upload parses the multipart body and populates req.body with + // Upload promises before Apollo handles the GraphQL operation. + req.body = await processRequest(req, res, uploadOptions); + return next(); + } catch (error) { + return next(error); + } + }; +}; + +export { createGraphQLUploadMiddleware, getGraphQLUpload }; diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index d24047329c..ec3b5ac71f 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -15,8 +15,8 @@ import { GraphQLUnionType, } from 'graphql'; import { toGlobalId } from 'graphql-relay'; -import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'; import Utils from '../../Utils'; +import { getGraphQLUpload } from '../helpers/graphqlUpload'; class TypeValidationError extends Error { constructor(value, type) { @@ -356,21 +356,25 @@ const FILE_INFO = new GraphQLObjectType({ }, }); -const FILE_INPUT = new GraphQLInputObjectType({ - name: 'FileInput', - description: - 'If this field is set to null the file will be unlinked (the file will not be deleted on cloud storage).', - fields: { - file: { - description: 'A File Scalar can be an url or a FileInfo object.', - type: FILE, - }, - upload: { - description: 'Use this field if you want to create a new file.', - type: GraphQLUpload, +let GraphQLUpload; +let FILE_INPUT; + +const createFileInputType = graphQLUpload => + new GraphQLInputObjectType({ + name: 'FileInput', + description: + 'If this field is set to null the file will be unlinked (the file will not be deleted on cloud storage).', + fields: { + file: { + description: 'A File Scalar can be an url or a FileInfo object.', + type: FILE, + }, + upload: { + description: 'Use this field if you want to create a new file.', + type: graphQLUpload, + }, }, - }, -}); + }); const GEO_POINT_FIELDS = { latitude: { @@ -1219,7 +1223,11 @@ const loadArrayResult = (parseGraphQLSchema, parseClassesArray) => { parseGraphQLSchema.graphQLTypes.push(ARRAY_RESULT); }; -const load = parseGraphQLSchema => { +const load = async parseGraphQLSchema => { + GraphQLUpload = await getGraphQLUpload(); + if (!FILE_INPUT) { + FILE_INPUT = createFileInputType(GraphQLUpload); + } parseGraphQLSchema.addGraphQLType(GraphQLUpload, true); parseGraphQLSchema.addGraphQLType(ANY, true); parseGraphQLSchema.addGraphQLType(OBJECT, true); diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js index 8439dfeb4f..8aa7c9a3bd 100644 --- a/src/GraphQL/loaders/filesMutations.js +++ b/src/GraphQL/loaders/filesMutations.js @@ -20,13 +20,14 @@ const handleUpload = async (upload, config) => { try { const ext = mime.getExtension(mimetype); const fullFileName = filename.endsWith(`.${ext}`) ? filename : `${filename}.${ext}`; + const encodedFileName = fullFileName.split('/').map(encodeURIComponent).join('/'); const serverUrl = new URL(config.serverURL); const fileInfo = await new Promise((resolve, reject) => { const req = request( { hostname: serverUrl.hostname, port: serverUrl.port, - path: `${serverUrl.pathname}/files/${fullFileName}`, + path: `${serverUrl.pathname}/files/${encodedFileName}`, method: 'POST', headers, }, @@ -37,7 +38,17 @@ const handleUpload = async (upload, config) => { }); res.on('end', () => { try { - resolve(JSON.parse(data)); + const parsedData = JSON.parse(data); + if (res.statusCode < 200 || res.statusCode >= 400) { + reject( + new Parse.Error( + parsedData.code || Parse.Error.FILE_SAVE_ERROR, + parsedData.error || data + ) + ); + return; + } + resolve(parsedData); } catch { reject(new Parse.Error(Parse.error, data)); } From 5a0811220adfc72f409c124ceedf7f427b4f50a9 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Tue, 26 May 2026 09:18:38 +0200 Subject: [PATCH 2/6] fix: ci --- package-lock.json | 173 ++++++++++++++++++++++++++-------------------- 1 file changed, 97 insertions(+), 76 deletions(-) diff --git a/package-lock.json b/package-lock.json index dd0a1ffd60..466ef98597 100644 --- a/package-lock.json +++ b/package-lock.json @@ -162,7 +162,6 @@ "integrity": "sha512-YM9lQpm0VfVco4DSyKooHS/fDTiKQcCHfxr7i3iL6a0kP/jNO5+4NFK6vtRDxaYisd5BrwOZHLJpPBnvRVpKPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@wry/caches": "^1.0.0", @@ -520,7 +519,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -3819,7 +3817,6 @@ "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", @@ -4316,7 +4313,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.11.0.tgz", "integrity": "sha512-GHoprlNQD51Xq2Ztd94HHV94MdFZQ3CVrpA04Fz8MVoHM0B7SlbmPEVIjwTbcv58z8QyjnrOuikS0rWF03k5dQ==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -4396,7 +4392,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", "dev": true, - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -5084,7 +5079,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", "dev": true, - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -5204,7 +5198,6 @@ "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.12.tgz", "integrity": "sha512-0mhiCR/4sZb00RVFJIUlMuiBkW3NMpVIW2Gse7noqEMoFGkvfPPAImEQbkBV8xga4KOPP4FdTRYuLLy32R1fPw==", "dev": true, - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^11.0.0", "@semantic-release/error": "^4.0.0", @@ -7593,7 +7586,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7991,7 +7983,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -8053,7 +8044,6 @@ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.1.tgz", "integrity": "sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==", "dev": true, - "peer": true, "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -8187,7 +8177,6 @@ "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", @@ -8285,7 +8274,6 @@ "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", @@ -9110,7 +9098,6 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9490,7 +9477,8 @@ "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==", "dev": true, - "optional": true + "optional": true, + "peer": true }, "node_modules/backoff": { "version": "2.5.0", @@ -9680,7 +9668,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -11807,7 +11794,6 @@ "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -12186,7 +12172,8 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", "dev": true, - "optional": true + "optional": true, + "peer": true }, "node_modules/execa": { "version": "5.1.1", @@ -12238,7 +12225,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -12282,7 +12268,6 @@ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", "license": "MIT", - "peer": true, "dependencies": { "ip-address": "10.1.0" }, @@ -13216,6 +13201,7 @@ "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", "license": "Apache-2.0", "optional": true, + "peer": true, "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", @@ -13231,6 +13217,7 @@ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -13241,6 +13228,7 @@ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" @@ -13258,6 +13246,7 @@ "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", "license": "Apache-2.0", "optional": true, + "peer": true, "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -13275,6 +13264,7 @@ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -13296,6 +13286,7 @@ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -13312,6 +13303,7 @@ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "license": "BlueOak-1.0.0", "optional": true, + "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -13322,6 +13314,7 @@ "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "glob": "^10.3.7" }, @@ -13338,6 +13331,7 @@ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "optional": true, + "peer": true, "engines": { "node": ">=14" }, @@ -13837,7 +13831,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -14800,7 +14793,8 @@ "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", "dev": true, - "optional": true + "optional": true, + "peer": true }, "node_modules/jackspeak": { "version": "3.4.3", @@ -14979,7 +14973,6 @@ "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@babel/parser": "^7.20.15", "@jsdoc/salty": "^0.2.1", @@ -16148,7 +16141,6 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, - "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -16176,7 +16168,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -16658,6 +16649,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "debug": "4" }, @@ -16728,6 +16720,7 @@ "dev": true, "license": "Apache-2.0", "optional": true, + "peer": true, "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", @@ -16738,6 +16731,22 @@ "node": ">=12" } }, + "node_modules/mongodb-runner/node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/mongodb-runner/node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -16745,6 +16754,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -16806,6 +16816,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -16827,7 +16838,8 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/mongodb-runner/node_modules/webidl-conversions": { "version": "3.0.1", @@ -16835,7 +16847,8 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true, "license": "BSD-2-Clause", - "optional": true + "optional": true, + "peer": true }, "node_modules/mongodb-runner/node_modules/whatwg-url": { "version": "5.0.0", @@ -16844,6 +16857,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -20500,7 +20514,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", @@ -20541,6 +20554,7 @@ "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.17.0.tgz", "integrity": "sha512-2Uio3Xfl5ldwJfls+RgGL+YbPcKQncWACWjYQFqlamvHZ4HJFjZhhZBbqd7jQ2LIkZYSvU90bm2dNW0rno+QFQ==", "license": "MIT", + "peer": true, "peerDependencies": { "pg": "^8" } @@ -20810,7 +20824,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", @@ -21816,7 +21829,6 @@ "integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -24005,7 +24017,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -25111,6 +25122,7 @@ "deprecated": "The `subscriptions-transport-ws` package is no longer maintained. We recommend you use `graphql-ws` instead. For help migrating Apollo software to `graphql-ws`, see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws For general help using `graphql-ws`, see https://github.com/enisdenjo/graphql-ws/blob/master/README.md", "dev": true, "optional": true, + "peer": true, "dependencies": { "backo2": "^1.0.2", "eventemitter3": "^3.1.0", @@ -25128,6 +25140,7 @@ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -25139,6 +25152,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8.3.0" }, @@ -25628,7 +25642,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -25883,7 +25896,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -26934,7 +26946,6 @@ "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.13.8.tgz", "integrity": "sha512-YM9lQpm0VfVco4DSyKooHS/fDTiKQcCHfxr7i3iL6a0kP/jNO5+4NFK6vtRDxaYisd5BrwOZHLJpPBnvRVpKPg==", "dev": true, - "peer": true, "requires": { "@graphql-typed-document-node/core": "^3.1.1", "@wry/caches": "^1.0.0", @@ -27166,7 +27177,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, - "peer": true, "requires": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -29411,7 +29421,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz", "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", "dev": true, - "peer": true, "requires": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", @@ -29812,7 +29821,6 @@ "version": "5.11.0", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.11.0.tgz", "integrity": "sha512-GHoprlNQD51Xq2Ztd94HHV94MdFZQ3CVrpA04Fz8MVoHM0B7SlbmPEVIjwTbcv58z8QyjnrOuikS0rWF03k5dQ==", - "peer": true, "requires": { "cluster-key-slot": "1.1.2" } @@ -29860,7 +29868,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", "dev": true, - "peer": true, "requires": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -30351,8 +30358,7 @@ "version": "9.1.6", "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", - "dev": true, - "peer": true + "dev": true }, "marked-terminal": { "version": "6.2.0", @@ -30421,7 +30427,6 @@ "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.12.tgz", "integrity": "sha512-0mhiCR/4sZb00RVFJIUlMuiBkW3NMpVIW2Gse7noqEMoFGkvfPPAImEQbkBV8xga4KOPP4FdTRYuLLy32R1fPw==", "dev": true, - "peer": true, "requires": { "@semantic-release/commit-analyzer": "^11.0.0", "@semantic-release/error": "^4.0.0", @@ -31983,8 +31988,7 @@ "picomatch": { "version": "4.0.3", "bundled": true, - "dev": true, - "peer": true + "dev": true } } }, @@ -32241,7 +32245,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "peer": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -32301,7 +32304,6 @@ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.1.tgz", "integrity": "sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==", "dev": true, - "peer": true, "requires": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -32430,7 +32432,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", "dev": true, - "peer": true, "requires": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", @@ -32484,7 +32485,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, - "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", @@ -32996,8 +32996,7 @@ "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "peer": true + "dev": true }, "acorn-jsx": { "version": "5.3.2", @@ -33280,7 +33279,8 @@ "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==", "dev": true, - "optional": true + "optional": true, + "peer": true }, "backoff": { "version": "2.5.0", @@ -33411,7 +33411,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, - "peer": true, "requires": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -34906,7 +34905,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, - "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -35160,7 +35158,8 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", "dev": true, - "optional": true + "optional": true, + "peer": true }, "execa": { "version": "5.1.1", @@ -35200,7 +35199,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "peer": true, "requires": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -35251,7 +35249,6 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", - "peer": true, "requires": { "ip-address": "10.1.0" } @@ -35864,6 +35861,7 @@ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", "optional": true, + "peer": true, "requires": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", @@ -35875,6 +35873,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "optional": true, + "peer": true, "requires": { "balanced-match": "^1.0.0" } @@ -35884,6 +35883,7 @@ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "optional": true, + "peer": true, "requires": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" @@ -35894,6 +35894,7 @@ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", "optional": true, + "peer": true, "requires": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -35906,6 +35907,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "optional": true, + "peer": true, "requires": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -35920,6 +35922,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "optional": true, + "peer": true, "requires": { "brace-expansion": "^2.0.1" } @@ -35928,13 +35931,15 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "optional": true + "optional": true, + "peer": true }, "rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "optional": true, + "peer": true, "requires": { "glob": "^10.3.7" } @@ -35943,7 +35948,8 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "optional": true + "optional": true, + "peer": true } } }, @@ -36304,8 +36310,7 @@ "graphql": { "version": "16.13.2", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", - "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", - "peer": true + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==" }, "graphql-list-fields": { "version": "2.0.4", @@ -36964,7 +36969,8 @@ "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", "dev": true, - "optional": true + "optional": true, + "peer": true }, "jackspeak": { "version": "3.4.3", @@ -37098,7 +37104,6 @@ "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", "dev": true, - "peer": true, "requires": { "@babel/parser": "^7.20.15", "@jsdoc/salty": "^0.2.1", @@ -37968,7 +37973,6 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, - "peer": true, "requires": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -37989,8 +37993,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", - "dev": true, - "peer": true + "dev": true }, "marked-terminal": { "version": "7.3.0", @@ -38310,6 +38313,7 @@ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "optional": true, + "peer": true, "requires": { "debug": "4" } @@ -38356,10 +38360,12 @@ "dev": true }, "gaxios": { - "version": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", "dev": true, "optional": true, + "peer": true, "requires": { "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", @@ -38367,12 +38373,25 @@ "node-fetch": "^2.6.9" } }, + "gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "optional": true, + "peer": true, "requires": { "agent-base": "6", "debug": "4" @@ -38395,6 +38414,7 @@ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "optional": true, + "peer": true, "requires": { "whatwg-url": "^5.0.0" } @@ -38404,14 +38424,16 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true, - "optional": true + "optional": true, + "peer": true }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true, - "optional": true + "optional": true, + "peer": true }, "whatwg-url": { "version": "5.0.0", @@ -38419,6 +38441,7 @@ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, "optional": true, + "peer": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -40906,7 +40929,6 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", - "peer": true, "requires": { "pg-cloudflare": "^1.3.0", "pg-connection-string": "^2.11.0", @@ -40931,6 +40953,7 @@ "version": "2.17.0", "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.17.0.tgz", "integrity": "sha512-2Uio3Xfl5ldwJfls+RgGL+YbPcKQncWACWjYQFqlamvHZ4HJFjZhhZBbqd7jQ2LIkZYSvU90bm2dNW0rno+QFQ==", + "peer": true, "requires": {} }, "pg-int8": { @@ -41110,7 +41133,6 @@ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, - "peer": true, "requires": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", @@ -41821,7 +41843,6 @@ "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-25.0.3.tgz", "integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==", "dev": true, - "peer": true, "requires": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -43230,8 +43251,7 @@ "picomatch": { "version": "4.0.3", "bundled": true, - "dev": true, - "peer": true + "dev": true } } }, @@ -44006,6 +44026,7 @@ "integrity": "sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==", "dev": true, "optional": true, + "peer": true, "requires": { "backo2": "^1.0.2", "eventemitter3": "^3.1.0", @@ -44019,7 +44040,8 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", "dev": true, - "optional": true + "optional": true, + "peer": true }, "ws": { "version": "7.5.10", @@ -44027,6 +44049,7 @@ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "optional": true, + "peer": true, "requires": {} } } @@ -44358,8 +44381,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "peer": true + "dev": true } } }, @@ -44539,8 +44561,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "peer": true + "dev": true }, "typescript-eslint": { "version": "8.58.0", From 063b85c380c7e42000733a257169fb6354247919 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Tue, 26 May 2026 09:27:37 +0200 Subject: [PATCH 3/6] fix: correct GraphQL file upload proxy error handling Reject non-2xx responses from the internal /files handoff and use Parse.Error.FILE_SAVE_ERROR when the response body is not valid JSON. Add regression tests for accented filenames in FilesController and Parse.File. Co-authored-by: Cursor --- spec/FilesController.spec.js | 7 +++++++ spec/ParseFile.spec.js | 22 ++++++++++++++++++++++ src/GraphQL/loaders/filesMutations.js | 6 +++--- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index 30acf7d13c..740077cbce 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -218,4 +218,11 @@ describe('FilesController', () => { expect(gridFSAdapter.validateFilename(fileName)).not.toBe(null); done(); }); + + it('should allow accented characters in file names', done => { + const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse'); + const fileName = 'café.txt'; + expect(gridFSAdapter.validateFilename(fileName)).toBe(null); + done(); + }); }); diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 5fa13ce9a9..664eb9df19 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -469,6 +469,28 @@ describe('Parse.File testing', () => { }); }); + it('allows accented filename characters', done => { + const headers = { + 'Content-Type': 'text/plain', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/files/caf%C3%A9.txt', + body: 'accented filename', + }).then(response => { + const b = response.data; + expect(b.name).toMatch(/_café.txt$/); + expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*caf%C3%A9.txt$/); + request({ url: b.url }).then(response => { + expect(response.text).toEqual('accented filename'); + done(); + }); + }, fail); + }); + it('validates filename length', done => { const headers = { 'Content-Type': 'text/plain', diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js index 8aa7c9a3bd..b81d8f1bfd 100644 --- a/src/GraphQL/loaders/filesMutations.js +++ b/src/GraphQL/loaders/filesMutations.js @@ -39,7 +39,7 @@ const handleUpload = async (upload, config) => { res.on('end', () => { try { const parsedData = JSON.parse(data); - if (res.statusCode < 200 || res.statusCode >= 400) { + if (res.statusCode < 200 || res.statusCode >= 300) { reject( new Parse.Error( parsedData.code || Parse.Error.FILE_SAVE_ERROR, @@ -49,8 +49,8 @@ const handleUpload = async (upload, config) => { return; } resolve(parsedData); - } catch { - reject(new Parse.Error(Parse.error, data)); + } catch (error) { + reject(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, error?.message || data)); } }); } From 81932311264a779faf7382a38aaaaa7e29e761c2 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Tue, 26 May 2026 10:04:48 +0200 Subject: [PATCH 4/6] fix: normalize Unicode filenames for file storage lookups Normalize file names to NFC before finalizing storage keys so uploads, reads, metadata lookups, and deletes resolve the same GridFS record across Unicode forms. Co-authored-by: Cursor --- spec/FileNameNormalization.spec.js | 97 +++++++++++++++++++++++ src/Adapters/Files/FilesAdapter.js | 11 +++ src/Adapters/Files/GridFSBucketAdapter.js | 8 +- src/Controllers/FilesController.js | 17 ++-- src/Routers/FilesRouter.js | 13 +-- 5 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 spec/FileNameNormalization.spec.js diff --git a/spec/FileNameNormalization.spec.js b/spec/FileNameNormalization.spec.js new file mode 100644 index 0000000000..c0882bf76c --- /dev/null +++ b/spec/FileNameNormalization.spec.js @@ -0,0 +1,97 @@ +'use strict'; + +const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter') + .GridFSBucketAdapter; +const request = require('../lib/request'); + +const databaseURI = 'mongodb://localhost:27017/parse'; + +describe_only_db('mongo')('Unicode filename normalization', () => { + beforeEach(async () => { + const gfsAdapter = new GridFSBucketAdapter(databaseURI); + const db = await gfsAdapter._connect(); + await db.dropDatabase(); + await gfsAdapter.handleShutdown(); + }); + + it('normalizes each path segment for direct GridFS adapter operations', async () => { + const gfsAdapter = new GridFSBucketAdapter(databaseURI); + const decomposedFilename = 'cafe\u0301.txt'; + const normalizedFilename = 'caf\u00e9.txt'; + const storedFilename = `docs/${normalizedFilename}`; + + await gfsAdapter.createFile(`docs/${decomposedFilename}`, 'normalized content', 'text/plain', { + metadata: {}, + }); + + const bucket = await gfsAdapter._getBucket(); + let documents = await bucket.find({ filename: storedFilename }).toArray(); + expect(documents.length).toBe(1); + + const metadata = await gfsAdapter.getMetadata(`docs/${decomposedFilename}`); + expect(metadata).toEqual({ metadata: {} }); + + const data = await gfsAdapter.getFileData(`docs/${decomposedFilename}`); + expect(data.toString('utf8')).toBe('normalized content'); + + await gfsAdapter.deleteFile(`docs/${decomposedFilename}`); + documents = await bucket.find({ filename: storedFilename }).toArray(); + expect(documents.length).toBe(0); + }); + + it('normalizes filenames across upload, metadata, download, and delete routes', async () => { + const gfsAdapter = new GridFSBucketAdapter(databaseURI); + await reconfigureServer({ + filesAdapter: gfsAdapter, + preserveFileName: true, + }); + + const decomposedFilename = 'cafe\u0301.txt'; + const normalizedFilename = 'caf\u00e9.txt'; + const requestedFilename = encodeURIComponent(decomposedFilename); + + const createResponse = await request({ + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + url: `http://localhost:8378/1/files/${requestedFilename}`, + body: 'normalized content', + }); + expect(createResponse.data.name).toBe(normalizedFilename); + expect(createResponse.data.url).toBe( + `http://localhost:8378/1/files/test/${encodeURIComponent(normalizedFilename)}` + ); + + const bucket = await gfsAdapter._getBucket(); + let documents = await bucket.find({ filename: normalizedFilename }).toArray(); + expect(documents.length).toBe(1); + + const metadataResponse = await request({ + method: 'GET', + url: `http://localhost:8378/1/files/test/metadata/${requestedFilename}`, + }); + expect(metadataResponse.data).toEqual({ metadata: {} }); + + const downloadResponse = await request({ + method: 'GET', + url: `http://localhost:8378/1/files/test/${requestedFilename}`, + }); + expect(downloadResponse.text).toBe('normalized content'); + + const deleteResponse = await request({ + method: 'DELETE', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + url: `http://localhost:8378/1/files/${requestedFilename}`, + }); + expect(deleteResponse.status).toBe(200); + + documents = await bucket.find({ filename: normalizedFilename }).toArray(); + expect(documents.length).toBe(0); + }); +}); diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js index af839dd937..7cb75b8d80 100644 --- a/src/Adapters/Files/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -101,6 +101,16 @@ export class FilesAdapter { // getMetadata(filename: string): Promise {} } +export function normalizeFilename(filename: any): any { + if (typeof filename !== 'string') { + return filename; + } + return filename + .split('/') + .map(segment => segment.normalize('NFC')) + .join('/'); +} + /** * Simple filename validation * @@ -108,6 +118,7 @@ export class FilesAdapter { * @returns {null|Parse.Error} */ export function validateFilename(filename): ?Parse.Error { + filename = normalizeFilename(filename); if (filename.length > 128) { return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.'); } diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 0236bec219..b244801c50 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -8,7 +8,7 @@ // @flow-disable-next import { MongoClient, GridFSBucket, Db } from 'mongodb'; -import { FilesAdapter, validateFilename } from './FilesAdapter'; +import { FilesAdapter, normalizeFilename, validateFilename } from './FilesAdapter'; import defaults, { ParseServerDatabaseOptions } from '../../defaults'; const crypto = require('crypto'); @@ -78,6 +78,7 @@ export class GridFSBucketAdapter extends FilesAdapter { // For a given config object, filename, and data, store a file // Returns a promise async createFile(filename: string, data, contentType, options = {}) { + filename = normalizeFilename(filename); const bucket = await this._getBucket(); const stream = await bucket.openUploadStream(filename, { metadata: options.metadata, @@ -135,6 +136,7 @@ export class GridFSBucketAdapter extends FilesAdapter { } async deleteFile(filename: string) { + filename = normalizeFilename(filename); const bucket = await this._getBucket(); const documents = await bucket.find({ filename }, { batchSize: this._batchSize }).toArray(); if (documents.length === 0) { @@ -148,6 +150,7 @@ export class GridFSBucketAdapter extends FilesAdapter { } async getFileData(filename: string) { + filename = normalizeFilename(filename); const bucket = await this._getBucket(); const stream = bucket.openDownloadStreamByName(filename); stream.read(); @@ -221,11 +224,13 @@ export class GridFSBucketAdapter extends FilesAdapter { } getFileLocation(config, filename) { + filename = normalizeFilename(filename); const encodedFilename = filename.split('/').map(encodeURIComponent).join('/'); return config.mount + '/files/' + config.applicationId + '/' + encodedFilename; } async getMetadata(filename) { + filename = normalizeFilename(filename); const bucket = await this._getBucket(); const files = await bucket.find({ filename }, { batchSize: this._batchSize }).toArray(); if (files.length === 0) { @@ -236,6 +241,7 @@ export class GridFSBucketAdapter extends FilesAdapter { } async handleFileStream(filename: string, req, res, contentType) { + filename = normalizeFilename(filename); const bucket = await this._getBucket(); const files = await bucket.find({ filename }, { batchSize: this._batchSize }).toArray(); if (files.length === 0) { diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 2c73eb365f..963896c044 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -1,7 +1,7 @@ // FilesController.js import { randomHexString } from '../cryptoUtils'; import AdaptableController from './AdaptableController'; -import { validateFilename, FilesAdapter } from '../Adapters/Files/FilesAdapter'; +import { normalizeFilename, validateFilename, FilesAdapter } from '../Adapters/Files/FilesAdapter'; import path from 'path'; const Parse = require('parse/node').Parse; @@ -10,8 +10,12 @@ const legacyFilesRegex = new RegExp( ); export class FilesController extends AdaptableController { + normalizeFilename(filename) { + return normalizeFilename(filename); + } + getFileData(config, filename) { - return this.adapter.getFileData(filename); + return this.adapter.getFileData(this.normalizeFilename(filename)); } async createFile(config, filename, data, contentType, options) { @@ -35,6 +39,8 @@ export class FilesController extends AdaptableController { delete options.directory; } + filename = this.normalizeFilename(filename); + // Fallback: buffer stream for adapters that don't support streaming if (typeof data?.pipe === 'function' && !this.adapter.supportsStreaming) { data = await new Promise((resolve, reject) => { @@ -54,12 +60,12 @@ export class FilesController extends AdaptableController { } deleteFile(config, filename) { - return this.adapter.deleteFile(filename); + return this.adapter.deleteFile(this.normalizeFilename(filename)); } getMetadata(filename) { if (typeof this.adapter.getMetadata === 'function') { - return this.adapter.getMetadata(filename); + return this.adapter.getMetadata(this.normalizeFilename(filename)); } return Promise.resolve({}); } @@ -110,10 +116,11 @@ export class FilesController extends AdaptableController { } handleFileStream(config, filename, req, res, contentType) { - return this.adapter.handleFileStream(filename, req, res, contentType); + return this.adapter.handleFileStream(this.normalizeFilename(filename), req, res, contentType); } validateFilename(filename) { + filename = this.normalizeFilename(filename); if (typeof this.adapter.validateFilename === 'function') { const error = this.adapter.validateFilename(filename); if (typeof error !== 'string') { diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index df6f710135..7c4e213401 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -219,9 +219,10 @@ export class FilesRouter { FilesRouter._validateFileDownload(req, config); + const filesController = config.filesController; let filename = FilesRouter._getFilenameFromParams(req); + filename = filesController.normalizeFilename(filename); try { - const filesController = config.filesController; const mime = (await import('mime')).default; let contentType = mime.getType(filename); let file = new Parse.File(filename, { base64: '' }, contentType); @@ -233,7 +234,7 @@ export class FilesRouter { fileAuth ); if (triggerResult?.file?._name) { - filename = triggerResult?.file?._name; + filename = filesController.normalizeFilename(triggerResult.file._name); contentType = mime.getType(filename); } @@ -401,7 +402,8 @@ export class FilesRouter { } } const filesController = config.filesController; - const { filename } = req.params; + const filename = filesController.normalizeFilename(req.params.filename); + req.params.filename = filename; const contentType = req.get('Content-type'); const error = filesController.validateFilename(filename); @@ -753,7 +755,7 @@ export class FilesRouter { } try { const { filesController } = req.config; - const filename = FilesRouter._getFilenameFromParams(req); + const filename = filesController.normalizeFilename(FilesRouter._getFilenameFromParams(req)); // run beforeDeleteFile trigger const file = new Parse.File(filename); file._url = await filesController.adapter.getFileLocation(req.config, filename); @@ -797,6 +799,7 @@ export class FilesRouter { FilesRouter._validateFileDownload(req, config); const { filesController } = config; let filename = FilesRouter._getFilenameFromParams(req); + filename = filesController.normalizeFilename(filename); const file = new Parse.File(filename, { base64: '' }); const fileAuth = req.auth; const triggerResult = await triggers.maybeRunFileTrigger( @@ -806,7 +809,7 @@ export class FilesRouter { fileAuth ); if (triggerResult?.file?._name) { - filename = triggerResult.file._name; + filename = filesController.normalizeFilename(triggerResult.file._name); } const data = await filesController.getMetadata(filename).catch(() => { res.status(200); From 7b2bc6e28934a31657e3d6bc4af9b846bf2dd8ce Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Tue, 26 May 2026 15:39:28 +0200 Subject: [PATCH 5/6] fix: harden filename validation for non-string and path inputs Reject non-string filenames before normalization, validate multi-segment file paths on download/delete/metadata routes, and drop combining marks from the filename allowlist after NFC normalization. Co-authored-by: Cursor --- spec/FileNameNormalization.spec.js | 19 +++++++++++ spec/FilesController.spec.js | 37 +++++++++++++++++++- src/Adapters/Files/FilesAdapter.js | 55 +++++++++++++++++++++++++++++- src/Controllers/FilesController.js | 14 +++++++- src/Routers/FilesRouter.js | 32 ++++++++++++++++- 5 files changed, 153 insertions(+), 4 deletions(-) diff --git a/spec/FileNameNormalization.spec.js b/spec/FileNameNormalization.spec.js index c0882bf76c..ac4696e0e7 100644 --- a/spec/FileNameNormalization.spec.js +++ b/spec/FileNameNormalization.spec.js @@ -94,4 +94,23 @@ describe_only_db('mongo')('Unicode filename normalization', () => { documents = await bucket.find({ filename: normalizedFilename }).toArray(); expect(documents.length).toBe(0); }); + + it('rejects path traversal in metadata download routes', async () => { + const gfsAdapter = new GridFSBucketAdapter(databaseURI); + await reconfigureServer({ + filesAdapter: gfsAdapter, + preserveFileName: true, + }); + + try { + await request({ + method: 'GET', + url: 'http://localhost:8378/1/files/test/metadata/..%2F..%2F..%2Fetc%2Fpasswd', + }); + fail('should have rejected path traversal'); + } catch (error) { + expect(error.status).toBe(400); + expect(error.data.code).toBe(Parse.Error.INVALID_FILE_NAME); + } + }); }); diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index 740077cbce..d8562a1a7c 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -5,6 +5,10 @@ const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter') .GridFSBucketAdapter; const Config = require('../lib/Config'); const FilesController = require('../lib/Controllers/FilesController').default; +const { + validateFilename, + validateFilepath, +} = require('../lib/Adapters/Files/FilesAdapter'); const databaseURI = 'mongodb://localhost:27017/parse'; const mockAdapter = { @@ -151,7 +155,7 @@ describe('FilesController', () => { return 'Bad file! No biscuit!'; }; const filesController = new FilesController(mockAdapter); - const error = filesController.validateFilename(); + const error = filesController.validateFilename('test.txt'); expect(typeof error).toBe('object'); expect(error.message.indexOf('biscuit')).toBe(13); expect(error.code).toBe(Parse.Error.INVALID_FILE_NAME); @@ -225,4 +229,35 @@ describe('FilesController', () => { expect(gridFSAdapter.validateFilename(fileName)).toBe(null); done(); }); + + it('rejects non-string filenames without throwing', () => { + for (const bad of [null, undefined, 42, {}]) { + const error = validateFilename(bad); + expect(error).not.toBeNull(); + expect(error.code).toBe(Parse.Error.INVALID_FILE_NAME); + expect(error.message).toMatch(/string/i); + } + }); + + it('rejects non-string filenames from FilesController without throwing', () => { + const filesController = new FilesController(mockAdapter); + const error = filesController.validateFilename(); + expect(typeof error).toBe('object'); + expect(error.code).toBe(Parse.Error.INVALID_FILE_NAME); + expect(error.message).toMatch(/string/i); + }); + + it('accepts NFC and NFD accented filenames after normalization', () => { + expect(validateFilename('caf\u00e9.txt')).toBeNull(); + expect(validateFilename('cafe\u0301.txt')).toBeNull(); + }); + + it('validates multi-segment filepaths', () => { + expect(validateFilepath('docs/caf\u00e9.txt')).toBeNull(); + expect(validateFilepath(`docs/cafe\u0301.txt`)).toBeNull(); + for (const bad of ['foo/../bar', '..', 'foo//bar', '/foo', 'foo/']) { + expect(validateFilepath(bad)).not.toBeNull(); + expect(validateFilepath(bad).code).toBe(Parse.Error.INVALID_FILE_NAME); + } + }); }); diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js index 7cb75b8d80..aad27c05fa 100644 --- a/src/Adapters/Files/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -101,6 +101,8 @@ export class FilesAdapter { // getMetadata(filename: string): Promise {} } +export const RESERVED_FILEPATH_SEGMENTS = ['metadata']; + export function normalizeFilename(filename: any): any { if (typeof filename !== 'string') { return filename; @@ -118,16 +120,67 @@ export function normalizeFilename(filename: any): any { * @returns {null|Parse.Error} */ export function validateFilename(filename): ?Parse.Error { + if (!filename || typeof filename !== 'string') { + return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename must be a string.'); + } filename = normalizeFilename(filename); if (filename.length > 128) { return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.'); } - const regx = /^[_\p{L}\p{N}][\p{L}\p{M}\p{N}@. ~_-]*$/u; + const regx = /^[_\p{L}\p{N}][\p{L}\p{N}@. ~_-]*$/u; if (!filename.match(regx)) { return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename contains invalid characters.'); } return null; } +/** + * Validate a stored file path that may contain directory segments. + * + * @param filepath + * @returns {null|Parse.Error} + */ +export function validateFilepath(filepath): ?Parse.Error { + if (!filepath || typeof filepath !== 'string') { + return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename must be a string.'); + } + const normalized = normalizeFilename(filepath); + if (normalized.includes('..')) { + return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'File path must not contain "..".'); + } + if (normalized.startsWith('/') || normalized.endsWith('/')) { + return new Parse.Error( + Parse.Error.INVALID_FILE_NAME, + 'File path must not start or end with "/".' + ); + } + if (normalized.includes('//')) { + return new Parse.Error( + Parse.Error.INVALID_FILE_NAME, + 'File path must not contain consecutive slashes.' + ); + } + const firstSegment = normalized.split('/')[0]; + if (RESERVED_FILEPATH_SEGMENTS.includes(firstSegment)) { + return new Parse.Error( + Parse.Error.INVALID_FILE_NAME, + `File path must not start with reserved segment "${firstSegment}".` + ); + } + for (const segment of normalized.split('/')) { + if (!segment) { + return new Parse.Error( + Parse.Error.INVALID_FILE_NAME, + 'File path must not contain empty segments.' + ); + } + const error = validateFilename(segment); + if (error) { + return error; + } + } + return null; +} + export default FilesAdapter; diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 963896c044..f79457299c 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -1,7 +1,12 @@ // FilesController.js import { randomHexString } from '../cryptoUtils'; import AdaptableController from './AdaptableController'; -import { normalizeFilename, validateFilename, FilesAdapter } from '../Adapters/Files/FilesAdapter'; +import { + normalizeFilename, + validateFilename, + validateFilepath, + FilesAdapter, +} from '../Adapters/Files/FilesAdapter'; import path from 'path'; const Parse = require('parse/node').Parse; @@ -120,6 +125,9 @@ export class FilesController extends AdaptableController { } validateFilename(filename) { + if (!filename || typeof filename !== 'string') { + return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename must be a string.'); + } filename = this.normalizeFilename(filename); if (typeof this.adapter.validateFilename === 'function') { const error = this.adapter.validateFilename(filename); @@ -130,6 +138,10 @@ export class FilesController extends AdaptableController { } return validateFilename(filename); } + + validateFilepath(filepath) { + return validateFilepath(filepath); + } } export default FilesController; diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 7c4e213401..e3a610cb5d 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -7,6 +7,7 @@ const triggers = require('../triggers'); const Utils = require('../Utils'); import { Readable } from 'stream'; import { createSanitizedHttpError } from '../Error'; +import { RESERVED_FILEPATH_SEGMENTS } from '../Adapters/Files/FilesAdapter'; /** * Wraps a readable stream in a Readable that enforces a byte size limit. @@ -83,7 +84,7 @@ export function createSizeLimitedStream(source, maxBytes) { // Segments that conflict with sub-routes under GET /files/:appId/*. If a file // directory starts with one of these, its URL would match the wrong route // handler. Update this list when adding new sub-routes to expressRouter(). -export const RESERVED_DIRECTORY_SEGMENTS = ['metadata']; +export const RESERVED_DIRECTORY_SEGMENTS = RESERVED_FILEPATH_SEGMENTS; export class FilesRouter { expressRouter({ maxUploadSize = '20Mb' } = {}) { @@ -222,6 +223,12 @@ export class FilesRouter { const filesController = config.filesController; let filename = FilesRouter._getFilenameFromParams(req); filename = filesController.normalizeFilename(filename); + const filepathError = filesController.validateFilepath(filename); + if (filepathError) { + res.status(400); + res.json({ code: filepathError.code, error: filepathError.message }); + return; + } try { const mime = (await import('mime')).default; let contentType = mime.getType(filename); @@ -235,6 +242,12 @@ export class FilesRouter { ); if (triggerResult?.file?._name) { filename = filesController.normalizeFilename(triggerResult.file._name); + const renamedPathError = filesController.validateFilepath(filename); + if (renamedPathError) { + res.status(400); + res.json({ code: renamedPathError.code, error: renamedPathError.message }); + return; + } contentType = mime.getType(filename); } @@ -756,6 +769,11 @@ export class FilesRouter { try { const { filesController } = req.config; const filename = filesController.normalizeFilename(FilesRouter._getFilenameFromParams(req)); + const filepathError = filesController.validateFilepath(filename); + if (filepathError) { + next(filepathError); + return; + } // run beforeDeleteFile trigger const file = new Parse.File(filename); file._url = await filesController.adapter.getFileLocation(req.config, filename); @@ -800,6 +818,12 @@ export class FilesRouter { const { filesController } = config; let filename = FilesRouter._getFilenameFromParams(req); filename = filesController.normalizeFilename(filename); + const filepathError = filesController.validateFilepath(filename); + if (filepathError) { + res.status(400); + res.json({ code: filepathError.code, error: filepathError.message }); + return; + } const file = new Parse.File(filename, { base64: '' }); const fileAuth = req.auth; const triggerResult = await triggers.maybeRunFileTrigger( @@ -810,6 +834,12 @@ export class FilesRouter { ); if (triggerResult?.file?._name) { filename = filesController.normalizeFilename(triggerResult.file._name); + const renamedPathError = filesController.validateFilepath(filename); + if (renamedPathError) { + res.status(400); + res.json({ code: renamedPathError.code, error: renamedPathError.message }); + return; + } } const data = await filesController.getMetadata(filename).catch(() => { res.status(200); From d2d265108922aaf093c30201a90090479d350add Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Tue, 26 May 2026 16:03:49 +0200 Subject: [PATCH 6/6] fix: feedbacks --- spec/FilesController.spec.js | 2 ++ src/Adapters/Files/FilesAdapter.js | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index d8562a1a7c..c980c503d2 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -255,6 +255,8 @@ describe('FilesController', () => { it('validates multi-segment filepaths', () => { expect(validateFilepath('docs/caf\u00e9.txt')).toBeNull(); expect(validateFilepath(`docs/cafe\u0301.txt`)).toBeNull(); + expect(validateFilepath('a..b.txt')).toBeNull(); + expect(validateFilepath('docs/a..b.txt')).toBeNull(); for (const bad of ['foo/../bar', '..', 'foo//bar', '/foo', 'foo/']) { expect(validateFilepath(bad)).not.toBeNull(); expect(validateFilepath(bad).code).toBe(Parse.Error.INVALID_FILE_NAME); diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js index aad27c05fa..d378353aef 100644 --- a/src/Adapters/Files/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -146,7 +146,8 @@ export function validateFilepath(filepath): ?Parse.Error { return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename must be a string.'); } const normalized = normalizeFilename(filepath); - if (normalized.includes('..')) { + const segments = normalized.split('/'); + if (segments.includes('..')) { return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'File path must not contain "..".'); } if (normalized.startsWith('/') || normalized.endsWith('/')) { @@ -161,14 +162,14 @@ export function validateFilepath(filepath): ?Parse.Error { 'File path must not contain consecutive slashes.' ); } - const firstSegment = normalized.split('/')[0]; + const firstSegment = segments[0]; if (RESERVED_FILEPATH_SEGMENTS.includes(firstSegment)) { return new Parse.Error( Parse.Error.INVALID_FILE_NAME, `File path must not start with reserved segment "${firstSegment}".` ); } - for (const segment of normalized.split('/')) { + for (const segment of segments) { if (!segment) { return new Parse.Error( Parse.Error.INVALID_FILE_NAME,