diff --git a/README.md b/README.md index 3f763dd..fe412de 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,17 @@ Detalle del despliegue y la secuencia de cutover: [`docs/rollout.md`](docs/rollo ```bash npm ci -npm test # 47 tests (vitest + mongodb-memory-server, sin infra externa) +npm test # 47 tests (vitest + fake-repo en memoria, sin infra externa) npm run typecheck npm run build # -> dist/ (+ plantillas) ``` +> **Nota MongoDB**: producción corre **MongoDB 3.2** (EOL), así que el servicio +> usa el driver `mongodb` **v3.7** (el moderno no conecta a 3.2) y no hay change +> streams (fase 1b usará polling). Los tests van contra un fake en memoria +> porque no hay mongod tan antiguo ejecutable en hosts modernos. Detalle en +> [`docs/legacy-behavior.md`](docs/legacy-behavior.md) §0. + ## Ejecución Config por env (ver [`.env.example`](.env.example)) o `config.json` (ver diff --git a/docs/legacy-behavior.md b/docs/legacy-behavior.md index dedd7e9..508d0a0 100644 --- a/docs/legacy-behavior.md +++ b/docs/legacy-behavior.md @@ -13,6 +13,24 @@ > **campo a campo**. Los tests de caracterización (`test/legacy-behavior.test.ts`) > lo verifican. +## 0. Entorno de datos (crítico) + +- **Producción corre MongoDB 3.2.11** (verificado en shiva vía `buildInfo`, + jul-2026). Es EOL desde 2018. Implica: + - El servicio usa el **driver `mongodb` v3.7** (el v6 exige servidor ≥ 4.2 y + **no conecta** a 3.2). No subir el driver sin subir antes el servidor. + - **No hay change streams** (requieren 3.6+) → la fase 1b debe usar el + **polling** de respaldo, no change streams. + - Los tests de integración corren contra un **fake en memoria** + (`test/fake-repo.ts`), porque en hosts modernos no se puede + descargar/ejecutar un mongod tan antiguo (glibc/libssl). +- Cifras reales del import (jul-2026): ~413 docs `notifications` pendientes de + FCM, 0 de email, **8176 users con `fireBaseToken`** (muchos llevarán ~2 años + muertos → alta tasa de token inválido en el primer envío real). +- Recomendación de fondo (fuera de fase 1a): planificar un **upgrade de MongoDB** + como fase propia; 3.2 bloquea drivers modernos, change streams y es un riesgo + de seguridad. Afecta a Meteor 1.6 y node-red, que dependen de él. + ## 1. Esquema del doc `notifications` Colección `notifications` (db `fuegos`, `idGeneration: 'MONGO'` → `_id` es diff --git a/package-lock.json b/package-lock.json index 5d3b7b9..269c96c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,16 +14,16 @@ "ioredis": "^5.4.2", "juice": "^11.0.0", "luxon": "^3.5.0", - "mongodb": "^6.12.0", + "mongodb": "^3.7.4", "nodemailer": "^6.9.16", "pino": "^9.5.0", "pino-pretty": "^13.0.0" }, "devDependencies": { "@types/luxon": "^3.4.2", + "@types/mongodb": "^3.6.20", "@types/node": "^22.10.2", "@types/nodemailer": "^6.4.17", - "mongodb-memory-server": "^11.2.0", "typescript": "^5.7.2", "vitest": "^2.1.8" }, @@ -728,15 +728,6 @@ "url": "https://opencollective.com/js-sdsl" } }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.12.tgz", - "integrity": "sha512-QAfAMwNgnYxZ2C6D1HgeP7Gc4i/uvJRim415PCIL9ptRxWMNbWeLBYb2/9R4pGKny/s1FVu2JA2cxCUBUOggrA==", - "license": "MIT", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", @@ -1270,6 +1261,16 @@ "node": ">= 10" } }, + "node_modules/@types/bson": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.5.tgz", + "integrity": "sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/caseless": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", @@ -1308,6 +1309,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mongodb": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", + "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bson": "*", + "@types/node": "*" + } + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -1353,21 +1365,6 @@ "license": "MIT", "optional": true }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "license": "MIT" - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "license": "MIT", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, "node_modules/@vitest/expect": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", @@ -1571,16 +1568,6 @@ "node": ">=12" } }, - "node_modules/async-mutex": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", - "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -1607,106 +1594,6 @@ "node": ">=8.0.0" } }, - "node_modules/b4a": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", - "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, - "node_modules/bare-events": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", - "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz", - "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", - "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bare-stream": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", - "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", - "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1736,6 +1623,46 @@ "node": "*" } }, + "node_modules/bl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/bl/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bl/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -1743,12 +1670,12 @@ "license": "ISC" }, "node_modules/bson": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", - "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", + "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==", "license": "Apache-2.0", "engines": { - "node": ">=16.20.1" + "node": ">=0.6.19" } }, "node_modules/buffer-equal-constant-time": { @@ -1836,19 +1763,6 @@ "node": ">= 0.4" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -1990,11 +1904,10 @@ "node": ">=18" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, "node_modules/cron-parser": { @@ -2393,16 +2306,6 @@ "node": ">=6" } }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -2440,13 +2343,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -2529,38 +2425,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/firebase-admin": { "version": "13.10.0", "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.10.0.tgz", @@ -2584,27 +2448,6 @@ "@google-cloud/storage": "^7.19.0" } }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/form-data": { "version": "2.5.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", @@ -2685,64 +2528,49 @@ } }, "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==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", "license": "Apache-2.0", - "optional": true, - "peer": true, "dependencies": { - "gaxios": "^5.0.0", + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" }, "engines": { - "node": ">=12" - } - }, - "node_modules/gcp-metadata/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" + "node": ">=18" } }, "node_modules/gcp-metadata/node_modules/gaxios": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", - "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.2.0.tgz", + "integrity": "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==", "license": "Apache-2.0", - "optional": true, - "peer": true, "dependencies": { "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" }, "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/gcp-metadata/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "node_modules/gcp-metadata/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "agent-base": "6", - "debug": "4" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": ">= 6" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "node_modules/get-caller-file": { @@ -2825,20 +2653,6 @@ "node": ">=18" } }, - "node_modules/google-auth-library/node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/google-auth-library/node_modules/node-fetch": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", @@ -3140,8 +2954,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/ioredis": { "version": "5.11.1", @@ -3201,6 +3014,12 @@ "license": "MIT", "optional": true }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/jose": { "version": "4.15.9", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", @@ -3312,19 +3131,6 @@ "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -3447,32 +3253,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3487,7 +3267,8 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/mensch": { "version": "0.3.4", @@ -3541,35 +3322,28 @@ } }, "node_modules/mongodb": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", - "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz", + "integrity": "sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw==", "license": "Apache-2.0", "dependencies": { - "@mongodb-js/saslprep": "^1.3.0", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.2" + "bl": "^2.2.1", + "bson": "^1.1.4", + "denque": "^1.4.1", + "optional-require": "^1.1.8", + "safe-buffer": "^5.1.2" }, "engines": { - "node": ">=16.20.1" + "node": ">=4" }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.3.2", - "socks": "^2.7.1" + "optionalDependencies": { + "saslprep": "^1.0.0" }, "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { + "aws4": { "optional": true }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { + "bson-ext": { "optional": true }, "kerberos": { @@ -3578,197 +3352,21 @@ "mongodb-client-encryption": { "optional": true }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", - "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", - "license": "Apache-2.0", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^14.1.0 || ^13.0.0" - } - }, - "node_modules/mongodb-memory-server": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-11.2.0.tgz", - "integrity": "sha512-506AD8qvClVx8Raw/WhAUUWBgIXPyi856iC01aa5vAzHmn6WOXC6ulvudkTF7oTMzJxkyA0A84VpD4BpyfqJ9w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "mongodb-memory-server-core": "11.2.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/mongodb-memory-server-core": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-11.2.0.tgz", - "integrity": "sha512-vOoDtn0JiLrHvZY81Rp/UtKXXK0rtJHZGZFVnccvJwYitPLNspO0Ty0grqFQOe7iAET8+GI4zAQcphg+R3vxQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-mutex": "^0.5.0", - "camelcase": "^6.3.0", - "debug": "^4.4.3", - "find-cache-dir": "^3.3.2", - "follow-redirects": "^1.16.0", - "https-proxy-agent": "^7.0.6", - "mongodb": "^7.2.0", - "new-find-package-json": "^2.0.0", - "semver": "^7.7.3", - "tar-stream": "^3.1.8", - "tslib": "^2.8.1", - "yauzl": "^3.3.1" - }, - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/mongodb-memory-server-core/node_modules/@types/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, - "node_modules/mongodb-memory-server-core/node_modules/bson": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-7.3.1.tgz", - "integrity": "sha512-h/C0qe6857pQhcSJHLfsR1uYGj98Ge3wKAD3Ed9KqH3wcVh+BM4Jq4xISD7vs9OPuT07n+q3QQVjslJ286j6ag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/mongodb-memory-server-core/node_modules/gaxios": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.2.0.tgz", - "integrity": "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/mongodb-memory-server-core/node_modules/gcp-metadata": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", - "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/mongodb-memory-server-core/node_modules/mongodb": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.5.0.tgz", - "integrity": "sha512-5FnrEDLnvp6ycUOGLNLLU33BfCx2qmp2mJjGPDwKLruYsVzXVSK5fsGpoDXvsXJwBfBsD7ebMRdawbDxC2814g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.4.11", - "bson": "^7.2.0", - "mongodb-connection-string-url": "^7.0.1" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.806.0", - "@mongodb-js/zstd": "^7.0.0", - "gcp-metadata": "^7.0.1", - "kerberos": "^7.0.0", - "mongodb-client-encryption": "^7.2.0", - "snappy": "^7.3.2", - "socks": "^2.8.6" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { + "mongodb-extjson": { "optional": true }, "snappy": { "optional": true - }, - "socks": { - "optional": true } } }, - "node_modules/mongodb-memory-server-core/node_modules/mongodb-connection-string-url": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", - "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", - "dev": true, + "node_modules/mongodb/node_modules/denque": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", + "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==", "license": "Apache-2.0", - "dependencies": { - "@types/whatwg-url": "^13.0.0", - "whatwg-url": "^14.1.0" - }, "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/mongodb-memory-server-core/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "node": ">=0.10" } }, "node_modules/ms": { @@ -3833,19 +3431,6 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, - "node_modules/new-find-package-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", - "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">=12.22.0" - } - }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -3893,31 +3478,6 @@ } } }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "optional": true - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/node-gyp-build-optional-packages": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", @@ -3982,6 +3542,18 @@ "wrappy": "1" } }, + "node_modules/optional-require": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.10.tgz", + "integrity": "sha512-0r3OB9EIQsP+a5HVATHq2ExIy2q/Vaffoo4IAikW1spCYswhLxqWQS0i3GwS3AdY/OIP4SWZHLGz8CMU558PGw==", + "license": "Apache-2.0", + "dependencies": { + "require-at": "^1.0.6" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3998,45 +3570,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -4086,16 +3619,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-expression-matcher": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", @@ -4129,13 +3652,6 @@ "node": ">= 14.16" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4213,19 +3729,6 @@ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/postcss": { "version": "8.5.18", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.18.tgz", @@ -4255,6 +3758,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -4318,15 +3827,6 @@ "once": "^1.3.1" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", @@ -4378,6 +3878,15 @@ "node": ">=4" } }, + "node_modules/require-at": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz", + "integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==", + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4493,6 +4002,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "license": "MIT", + "optional": true, + "dependencies": { + "sparse-bitfield": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/secure-json-parse": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", @@ -4570,6 +4092,7 @@ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", "license": "MIT", + "optional": true, "dependencies": { "memory-pager": "^1.0.2" } @@ -4620,18 +4143,6 @@ "license": "MIT", "optional": true }, - "node_modules/streamx": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", - "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", - "dev": true, - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -4705,19 +4216,6 @@ "license": "MIT", "optional": true }, - "node_modules/tar-stream": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", - "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, "node_modules/teeny-request": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", @@ -4762,26 +4260,6 @@ "node": ">= 6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", @@ -4836,16 +4314,11 @@ } }, "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } + "optional": true }, "node_modules/tslib": { "version": "2.8.1", @@ -4899,8 +4372,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/uuid": { "version": "9.0.1", @@ -5113,13 +4585,11 @@ } }, "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } + "optional": true }, "node_modules/websocket-driver": { "version": "0.7.5", @@ -5167,16 +4637,14 @@ } }, "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "license": "MIT", + "optional": true, "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, "node_modules/why-is-node-running": { @@ -5287,19 +4755,6 @@ "node": ">=12" } }, - "node_modules/yauzl": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", - "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index ba106fd..8dfdf21 100644 --- a/package.json +++ b/package.json @@ -22,16 +22,16 @@ "ioredis": "^5.4.2", "juice": "^11.0.0", "luxon": "^3.5.0", - "mongodb": "^6.12.0", + "mongodb": "^3.7.4", "nodemailer": "^6.9.16", "pino": "^9.5.0", "pino-pretty": "^13.0.0" }, "devDependencies": { "@types/luxon": "^3.4.2", + "@types/mongodb": "^3.6.20", "@types/node": "^22.10.2", "@types/nodemailer": "^6.4.17", - "mongodb-memory-server": "^11.2.0", "typescript": "^5.7.2", "vitest": "^2.1.8" } diff --git a/src/db.ts b/src/db.ts index 4d50fd2..c2aa0c8 100644 --- a/src/db.ts +++ b/src/db.ts @@ -12,9 +12,12 @@ export interface Repos { } export async function connect(cfg: Config): Promise { + // Production MongoDB is 3.2 (see docs/legacy-behavior.md). We pin the v3.x + // driver; useUnifiedTopology is required, and retryWrites (3.6+) is omitted. const client = new MongoClient(cfg.mongoUrl, { - // The service only writes provenance/idempotency; prefer safety. - retryWrites: true, + useUnifiedTopology: true, + useNewUrlParser: true, + serverSelectionTimeoutMS: 10_000, }); await client.connect(); const db = client.db(cfg.dbName); diff --git a/src/poller.ts b/src/poller.ts index 1fe15eb..9bf339f 100644 --- a/src/poller.ts +++ b/src/poller.ts @@ -1,5 +1,5 @@ import type { Queue } from 'bullmq'; -import type { Filter } from 'mongodb'; +import type { FilterQuery } from 'mongodb'; import type { Config } from './config.js'; import type { Repos } from './db.js'; import type { Logger } from './logger.js'; @@ -8,7 +8,7 @@ import { jobId } from './queue.js'; import type { Channel, NotificationDoc, SendJob } from './types.js'; /** Pending-doc filter for a channel — the CORRECT fields (old cron had a typo). */ -export function pendingFilter(channel: Channel): Filter { +export function pendingFilter(channel: Channel): FilterQuery { return channel === 'fcm' ? { type: 'mobile', notified: { $ne: true } } : { type: 'web', emailNotified: { $ne: true } }; diff --git a/src/sends.ts b/src/sends.ts index 0babd5c..285f322 100644 --- a/src/sends.ts +++ b/src/sends.ts @@ -1,7 +1,12 @@ -import { MongoServerError, type ObjectId } from 'mongodb'; +import type { ObjectId } from 'mongodb'; import type { Repos } from './db.js'; import type { Channel, OperatingMode, SendRecord } from './types.js'; +/** Duplicate-key error (unique index violated). Works across driver versions. */ +function isDuplicateKey(err: unknown): boolean { + return typeof err === 'object' && err !== null && (err as { code?: number }).code === 11000; +} + /** * Persistent idempotency. `reserve()` inserts a placeholder into * `notification_sends`; the unique index (notificationId, channel) makes a @@ -28,7 +33,7 @@ export async function reserve( await repos.sends.insertOne(rec); return true; } catch (err) { - if (err instanceof MongoServerError && err.code === 11000) { + if (isDuplicateKey(err)) { return false; // already reserved/sent — idempotent skip } throw err; diff --git a/test/fake-repo.ts b/test/fake-repo.ts new file mode 100644 index 0000000..5d77ac1 --- /dev/null +++ b/test/fake-repo.ts @@ -0,0 +1,165 @@ +/** + * In-memory fake of the Mongo collections used by the service. Production runs + * MongoDB 3.2, for which no mongod binary is downloadable/runnable on modern + * dev/CI hosts (glibc/libssl). So the DB integration tests run against this + * fake, which faithfully emulates the subset of behaviour the code relies on: + * the query operators used ($ne/$in/$gte/$exists/equality), $set/$unset updates, + * projection/limit, and — crucially — unique-index duplicate-key errors (11000) + * that back the idempotency guarantee. + */ +import type { Repos } from '../src/db.js'; + +type Doc = Record; + +function isObjectId(v: any): boolean { + return v != null && typeof v === 'object' && typeof v.equals === 'function' && v._bsontype === 'ObjectID'; +} + +function valEq(a: any, b: any): boolean { + if (a === b) return true; + if (a == null || b == null) return a === b; + if (isObjectId(a)) { + try { + return a.equals(b); + } catch { + return false; + } + } + if (isObjectId(b)) return b.equals(a); + if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime(); + return false; +} + +function isOperatorObject(cond: any): boolean { + if (cond == null || typeof cond !== 'object') return false; + if (cond instanceof Date || isObjectId(cond)) return false; + const keys = Object.keys(cond); + return keys.length > 0 && keys.every((k) => k.startsWith('$')); +} + +function matchField(docVal: any, cond: any): boolean { + if (isOperatorObject(cond)) { + return Object.entries(cond).every(([op, v]) => { + switch (op) { + case '$ne': + return !valEq(docVal, v); + case '$in': + return Array.isArray(v) && v.some((x) => valEq(docVal, x)); + case '$gte': + return docVal != null && docVal >= (v as any); + case '$lte': + return docVal != null && docVal <= (v as any); + case '$exists': + return (docVal !== undefined) === v; + default: + throw new Error(`fake-repo: unsupported operator ${op}`); + } + }); + } + return valEq(docVal, cond); +} + +function matches(doc: Doc, filter: Doc): boolean { + return Object.entries(filter).every(([k, cond]) => matchField(doc[k], cond)); +} + +function applyUpdate(doc: Doc, update: Doc): void { + if (update.$set) Object.assign(doc, update.$set); + if (update.$unset) for (const k of Object.keys(update.$unset)) delete doc[k]; +} + +class FakeCollection { + docs: Doc[] = []; + private uniqueFields: string[] | null = null; + + async createIndex(spec: Doc, opts?: { unique?: boolean }): Promise { + if (opts?.unique) this.uniqueFields = Object.keys(spec); + return 'idx'; + } + + private violatesUnique(doc: Doc): boolean { + if (!this.uniqueFields) return false; + return this.docs.some((d) => this.uniqueFields!.every((f) => valEq(d[f], doc[f]))); + } + + async insertOne(doc: Doc): Promise<{ insertedId: any }> { + if (this.violatesUnique(doc)) { + const err: any = new Error('E11000 duplicate key error'); + err.code = 11000; + throw err; + } + const copy = { ...doc }; + this.docs.push(copy); + return { insertedId: copy._id }; + } + + async insertMany(docs: Doc[]): Promise<{ insertedCount: number }> { + for (const d of docs) await this.insertOne(d); + return { insertedCount: docs.length }; + } + + async findOne(filter: Doc = {}): Promise { + return this.docs.find((d) => matches(d, filter)) ?? null; + } + + async updateOne(filter: Doc, update: Doc): Promise<{ modifiedCount: number }> { + const d = this.docs.find((x) => matches(x, filter)); + if (!d) return { modifiedCount: 0 }; + applyUpdate(d, update); + return { modifiedCount: 1 }; + } + + async updateMany(filter: Doc, update: Doc): Promise<{ modifiedCount: number }> { + const ds = this.docs.filter((x) => matches(x, filter)); + for (const d of ds) applyUpdate(d, update); + return { modifiedCount: ds.length }; + } + + async deleteOne(filter: Doc): Promise<{ deletedCount: number }> { + const i = this.docs.findIndex((d) => matches(d, filter)); + if (i === -1) return { deletedCount: 0 }; + this.docs.splice(i, 1); + return { deletedCount: 1 }; + } + + async deleteMany(filter: Doc = {}): Promise<{ deletedCount: number }> { + const before = this.docs.length; + this.docs = this.docs.filter((d) => !matches(d, filter)); + return { deletedCount: before - this.docs.length }; + } + + async countDocuments(filter: Doc = {}): Promise { + return this.docs.filter((d) => matches(d, filter)).length; + } + + find(filter: Doc = {}, _opts?: Doc) { + let result = this.docs.filter((d) => matches(d, filter)); + const cursor = { + project() { + return cursor; + }, + limit(n: number) { + result = result.slice(0, n); + return cursor; + }, + async toArray() { + return result.map((d) => ({ ...d })); + }, + }; + return cursor; + } +} + +export function makeRepos(): Repos { + const notifications = new FakeCollection(); + const users = new FakeCollection(); + const sends = new FakeCollection(); + return { + client: {} as any, + db: {} as any, + notifications: notifications as any, + users: users as any, + sends: sends as any, + close: async () => {}, + }; +} diff --git a/test/handlers.test.ts b/test/handlers.test.ts index dca5263..5220415 100644 --- a/test/handlers.test.ts +++ b/test/handlers.test.ts @@ -1,30 +1,14 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import type { MongoMemoryServer } from 'mongodb-memory-server'; +import { beforeEach, describe, expect, it } from 'vitest'; import type { Repos } from '../src/db.js'; import type { Config } from '../src/config.js'; import { processEmailJob, processFcmJob, type Deps } from '../src/handlers.js'; -import { aNotif, aUser, fakeFcm, fakeMailer, silentLog, startMongo } from './helpers.js'; +import { aNotif, aUser, fakeFcm, fakeMailer, freshRepos, silentLog, testCfg } from './helpers.js'; -let server: MongoMemoryServer; let repos: Repos; -let baseCfg: Config; - -beforeAll(async () => { - const s = await startMongo(); - server = s.server; - repos = s.repos; - baseCfg = s.cfg; -}); - -afterAll(async () => { - await repos.close(); - await server.stop(); -}); +const baseCfg: Config = testCfg(); beforeEach(async () => { - await repos.notifications.deleteMany({}); - await repos.users.deleteMany({}); - await repos.sends.deleteMany({}); + repos = await freshRepos(); }); function deps(over: Partial = {}): Deps { diff --git a/test/helpers.ts b/test/helpers.ts index 411cc44..ed870d5 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,21 +1,20 @@ -import { MongoMemoryServer } from 'mongodb-memory-server'; import { pino } from 'pino'; import { ObjectId } from 'mongodb'; -import { connect, ensureIndexes, type Repos } from '../src/db.js'; +import type { Repos } from '../src/db.js'; import type { Config } from '../src/config.js'; import type { FcmClient, FcmSendResult } from '../src/fcm.js'; import type { Mailer } from '../src/mailer.js'; import type { EmailContent, PushPayload } from '../src/messages.js'; import type { NotificationDoc, UserDoc } from '../src/types.js'; +import { makeRepos } from './fake-repo.js'; export const silentLog = pino({ level: 'silent' }); -export async function startMongo(): Promise<{ server: MongoMemoryServer; repos: Repos; cfg: Config }> { - const server = await MongoMemoryServer.create(); - const cfg = testCfg({ mongoUrl: server.getUri(), dbName: 'fuegos' }); - const repos = await connect(cfg); - await ensureIndexes(repos); - return { server, repos, cfg }; +/** Fresh in-memory repos with the idempotency unique index applied. */ +export async function freshRepos(): Promise { + const repos = makeRepos(); + await repos.sends.createIndex({ notificationId: 1, channel: 1 }, { unique: true, name: 'uniq' }); + return repos; } export function testCfg(over: Partial = {}): Config { diff --git a/test/poller.test.ts b/test/poller.test.ts index b5229bd..ae41c0f 100644 --- a/test/poller.test.ts +++ b/test/poller.test.ts @@ -1,29 +1,17 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; import type { Queue } from 'bullmq'; -import type { MongoMemoryServer } from 'mongodb-memory-server'; import type { Repos } from '../src/db.js'; import type { Config } from '../src/config.js'; import { createPoller } from '../src/poller.js'; import { pendingFilter } from '../src/poller.js'; import type { Channel, SendJob } from '../src/types.js'; -import { aNotif, silentLog, startMongo } from './helpers.js'; +import { aNotif, freshRepos, silentLog, testCfg } from './helpers.js'; -let server: MongoMemoryServer; let repos: Repos; -let baseCfg: Config; +const baseCfg: Config = testCfg(); -beforeAll(async () => { - const s = await startMongo(); - server = s.server; - repos = s.repos; - baseCfg = s.cfg; -}); -afterAll(async () => { - await repos.close(); - await server.stop(); -}); beforeEach(async () => { - await repos.notifications.deleteMany({}); + repos = await freshRepos(); }); /** Fake queue recording add() calls. */