From 4fd2671454674de391da51808f2440f53dbd70b0 Mon Sep 17 00:00:00 2001 From: homen Date: Tue, 28 Jul 2026 23:08:56 -0700 Subject: [PATCH] Harden npm dependencies, Convex auth, tests, vendor, docs, and evidence --- convex/authEmail.ts | 3 +- docs/security/EXCELJS_DEPENDENCY_HARDENING.md | 104 ++++ .../auth-email-canonicalization/after.txt | 14 + .../auth-email-canonicalization/before.txt | 14 + evidence/npm-audit-remediation/after.txt | 58 ++ evidence/npm-audit-remediation/before.txt | 75 +++ package-lock.json | 568 +----------------- package.json | 12 +- tests/authEmailVerification.test.ts | 3 + tests/exceljsDependencyCompatibility.test.ts | 171 ++++++ vendor/exceljs-security/fstream/index.cjs | 13 + vendor/exceljs-security/fstream/package.json | 10 + 12 files changed, 505 insertions(+), 540 deletions(-) create mode 100644 docs/security/EXCELJS_DEPENDENCY_HARDENING.md create mode 100644 evidence/auth-email-canonicalization/after.txt create mode 100644 evidence/auth-email-canonicalization/before.txt create mode 100644 evidence/npm-audit-remediation/after.txt create mode 100644 evidence/npm-audit-remediation/before.txt create mode 100644 tests/exceljsDependencyCompatibility.test.ts create mode 100644 vendor/exceljs-security/fstream/index.cjs create mode 100644 vendor/exceljs-security/fstream/package.json diff --git a/convex/authEmail.ts b/convex/authEmail.ts index 92b4c302..eb57ea9d 100644 --- a/convex/authEmail.ts +++ b/convex/authEmail.ts @@ -11,7 +11,8 @@ type MailEnvironment = { export function normalizeAuthEmail(value: unknown): string { if (typeof value !== "string") throw new Error("invalid_email"); - const email = value.trim().toLowerCase(); + if (value.length > 512) throw new Error("invalid_email"); + const email = value.normalize("NFKC").trim().toLowerCase(); if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { throw new Error("invalid_email"); } diff --git a/docs/security/EXCELJS_DEPENDENCY_HARDENING.md b/docs/security/EXCELJS_DEPENDENCY_HARDENING.md new file mode 100644 index 00000000..6b01849d --- /dev/null +++ b/docs/security/EXCELJS_DEPENDENCY_HARDENING.md @@ -0,0 +1,104 @@ +# ExcelJS dependency hardening + +## Decision + +Keep the public `exceljs@4.4.0` API and harden its two ZIP dependency paths +without changing the workbook contract: + +- `archiver` -> `@excel.js/archiver@0.0.5` +- `unzipper` -> official `unzipper@0.10.14` +- `unzipper -> fstream` -> NodeRoom's local + `fstream@1.0.12+noderoom.fail-closed.1` compatibility package + +The archive writer is the narrow, ExcelJS-compatible package published from +[`excel-js/excel-js`](https://github.com/excel-js/excel-js). The reader stays on +the exact parser version ExcelJS 4.4.0 already used because its entry ordering is +part of ExcelJS's shared-string streaming behavior. ExcelJS only invokes +`unzipper.Parse({ forceStream: true })`; it never invokes `Extract` or `Open`, +which are the `fstream`-backed extraction APIs. The local compatibility package +therefore throws on every `fstream` operation. If a future code path attempts +extraction, it fails closed instead of silently reintroducing filesystem writes. + +The direct `unzipper` and local `fstream` dependency entries are deterministic +anchors for npm's `$dependency` override syntax. The package lock records the +exact writer tarball integrity and the explicitly NodeRoom-owned build identity +for the local compatibility boundary. Build metadata keeps the +identity visibly local while satisfying `unzipper`'s `^1.0.12` compatibility +range; a prerelease suffix would fall outside that range and make `npm ci` +resolve the vulnerable upstream package again. + +## Root cause + +The July 2026 `brace-expansion` denial-of-service advisory affected every +release through `5.0.7`. ExcelJS 4.4.0 reached vulnerable legacy releases over +two independent production paths: + +```text +exceljs -> archiver -> archiver-utils/readdir-glob -> glob/minimatch -> brace-expansion +exceljs -> unzipper -> fstream -> rimraf -> glob/minimatch -> brace-expansion +``` + +The same release gate also found three critical Auth.js advisories because the +application pinned `@auth/core@0.41.1`; the patched compatible version is +`0.41.3`. NodeRoom's custom email-provider normalizer also validated before +Unicode canonicalization, so it now applies bounded NFKC normalization before +checking the one-`@` address shape. This closes the application-specific form of +the Auth.js homoglyph advisory instead of relying on the dependency bump alone. + +The clean-lock verification also surfaced newer high-severity advisories in the +development graph. The lock now selects `fast-uri@3.1.4`, +`postcss@8.5.24`, and its compatible `nanoid@3.3.16` transitive without changing +the declared dependency ranges. + +## Alternatives rejected + +- `npm audit fix --force` proposed downgrading ExcelJS to 4.1.1 and did not + provide a compatibility argument. +- Overriding only `brace-expansion` is unsafe because legacy Minimatch expects a + callable CommonJS export, while patched brace-expansion 5 exposes `expand` as + a named export. +- Overriding ExcelJS to `archiver@8` is not compatible: ExcelJS expects a + callable CommonJS factory and passes its legacy `StreamBuf`, which Archiver 8 + rejects. +- Upgrading to `unzipper@0.12` can reorder ZIP entries in the streaming reader + and break shared-string caching. +- The initially evaluated `@excel.js/unzipper@0.0.2` preserves the API shape but + provides no reliability advantage. Stress diagnostics reproduced ExcelJS + 4.4's existing short-archive shared-string timing race with the original, + scoped, and current `0.12` parser implementations. NodeRoom's application + paths use `Workbook.xlsx.load/readFile`, not `WorkbookReader`, so the gate + exercises the real application reader concurrently and retains one sustained + `WorkbookReader` round trip to protect the dependency contract. +- Replacing `fstream` with an implementation that extracts files would preserve + an unused attack surface. The selected boundary makes the supported parse-only + contract explicit and rejects extraction. + +## Verification + +Run: + +```powershell +npm ci +npm audit --audit-level=moderate +npm audit --omit=dev --audit-level=moderate +npm test -- --run tests/authEmailVerification.test.ts +npm test -- --run tests/exceljsDependencyCompatibility.test.ts tests/artifactXlsxExport.test.ts tests/spreadsheetParser.test.ts +npm run floor +npm run prod:gate +``` + +`tests/exceljsDependencyCompatibility.test.ts` protects the non-obvious +compatibility boundary with a 1,024-row streaming write/read, 16 waves of four +concurrent writers loaded through NodeRoom's application reader, truncated-input +failure, exact package-resolution checks, and a fail-closed extraction +assertion. + +## Primary advisories + +- [Auth.js malformed Bearer handling](https://github.com/advisories/GHSA-xmf8-cvqr-rfgj) +- [Auth.js email normalization](https://github.com/advisories/GHSA-7rqj-j65f-68wh) +- [Auth.js provider-bound OAuth cookies](https://github.com/advisories/GHSA-x445-f3h2-j279) +- [brace-expansion denial of service](https://github.com/advisories/GHSA-mh99-v99m-4gvg) +- [fast-uri host confusion](https://github.com/advisories/GHSA-v2hh-gcrm-f6hx) +- [fast-uri IDN canonicalization](https://github.com/advisories/GHSA-4c8g-83qw-93j6) +- [PostCSS source-map path traversal](https://github.com/advisories/GHSA-r28c-9q8g-f849) diff --git a/evidence/auth-email-canonicalization/after.txt b/evidence/auth-email-canonicalization/after.txt new file mode 100644 index 00000000..9e8a055c --- /dev/null +++ b/evidence/auth-email-canonicalization/after.txt @@ -0,0 +1,14 @@ +Observable: email-provider identifier canonicalization after remediation +Command: npm test -- --run tests/authEmailVerification.test.ts +Exit: 0 + +Test Files: 1 passed (1) +Tests: 4 passed (4) + +The scenario now proves: +- a single fullwidth @ canonicalizes to the intended ASCII address; +- a second separator exposed by NFKC is rejected; +- malformed input is rejected; +- oversized input is rejected before Unicode normalization; +- missing mail transport still fails closed; and +- the configured transport receives only the normalized address and challenge. diff --git a/evidence/auth-email-canonicalization/before.txt b/evidence/auth-email-canonicalization/before.txt new file mode 100644 index 00000000..46818b36 --- /dev/null +++ b/evidence/auth-email-canonicalization/before.txt @@ -0,0 +1,14 @@ +Observable: email-provider identifier canonicalization before the @auth/core 0.41.3 remediation is complete +Command: npm test -- --run tests/authEmailVerification.test.ts +Exit: 1 + +Test Files: 1 failed (1) +Tests: 1 failed | 3 passed (4) + +Failed scenario: +normalizeAuthEmail(" Person@Example.COM ") should canonicalize the fullwidth +separator before validation, but the implementation throws "invalid_email". + +The same pre-fix implementation validates the raw string before Unicode +normalization, so a second fullwidth separator can be introduced downstream: +"victim@example.com@attacker.example". diff --git a/evidence/npm-audit-remediation/after.txt b/evidence/npm-audit-remediation/after.txt new file mode 100644 index 00000000..8a2bcff8 --- /dev/null +++ b/evidence/npm-audit-remediation/after.txt @@ -0,0 +1,58 @@ +Observable: production dependency audit after remediation +Base commit: b800709408a3a33b672f239fdbf7d5f348b6d282 +Command: npm audit --omit=dev --audit-level=moderate +Exit: 0 + +found 0 vulnerabilities + +The stricter all-dependency command also passes: +Command: npm audit --audit-level=moderate +Exit: 0 + +found 0 vulnerabilities + +Exact installed production paths: +noderoom@0.1.1 ++-- @auth/core@0.41.3 ++-- @convex-dev/auth@0.0.94 +| `-- @auth/core@0.41.3 deduped ++-- exceljs@4.4.0 overridden +| +-- archiver@npm:@excel.js/archiver@0.0.5 overridden +| `-- unzipper@0.10.14 deduped ++-- fstream@1.0.12+noderoom.fail-closed.1 -> vendor/exceljs-security/fstream +`-- unzipper@0.10.14 overridden + `-- fstream@1.0.12+noderoom.fail-closed.1 deduped -> vendor/exceljs-security/fstream + +Compatibility proof: +Command: npm test -- --run tests/authEmailVerification.test.ts tests/exceljsDependencyCompatibility.test.ts tests/artifactXlsxExport.test.ts tests/spreadsheetParser.test.ts +Exit: 0 +Test Files: 4 passed +Tests: 19 passed + +Repeated compatibility stress: +Command: 12 consecutive runs of tests/exceljsDependencyCompatibility.test.ts +Exit: 0 +Runs: 12/12 +Scenario per run: 1,024-row stream; 16 waves x 4 concurrent writers; truncated input; exact dependency/fail-closed resolution + +Canonical floor: +Command: npm run floor +Exit: 0 +Test Files: 373 passed +Tests: 2571 passed + +Full production gate: +Command: npm run prod:gate +Exit: 0 +Security gate: pass (source and dist) +Design audit: pass (existing guidance-only token warnings retained) +QA matrix: current +Content fluency: pass +Proof staleness: pass +Fresh-room proofs: FR-010 and FR-020 pass +SLO gate: pass (8/8 completed, 0 errors, p95 18ms) +Root and Convex typechecks: pass +Test Files: 373 passed +Tests: 2571 passed +Product-memory Playwright: pass +Production build: pass diff --git a/evidence/npm-audit-remediation/before.txt b/evidence/npm-audit-remediation/before.txt new file mode 100644 index 00000000..92a5d490 --- /dev/null +++ b/evidence/npm-audit-remediation/before.txt @@ -0,0 +1,75 @@ +Observable: production dependency audit on clean origin/main before remediation +Commit: b800709408a3a33b672f239fdbf7d5f348b6d282 +Command: npm audit --omit=dev --audit-level=moderate +Exit: 1 + +# npm audit report + +@auth/core <=0.41.2 +Severity: critical +Auth.js: getToken() throws an uncaught exception on malformed Bearer authorization headers - https://github.com/advisories/GHSA-xmf8-cvqr-rfgj +Auth.js: Email normalizer validates the address before Unicode normalization, allowing a homoglyph @ bypass - https://github.com/advisories/GHSA-7rqj-j65f-68wh +Auth.js: OAuth state, nonce, and PKCE check cookies are not bound to the provider that created them - https://github.com/advisories/GHSA-x445-f3h2-j279 +fix available via `npm audit fix --force` +Will install @auth/core@0.41.3, which is outside the stated dependency range +node_modules/@auth/core + +brace-expansion <=5.0.7 +Severity: high +brace-expansion: DoS via unbounded expansion length causing an out-of-memory process crash - https://github.com/advisories/GHSA-mh99-v99m-4gvg +fix available via `npm audit fix --force` +Will install exceljs@4.1.1, which is a breaking change +node_modules/minimatch/node_modules/brace-expansion +node_modules/readdir-glob/node_modules/brace-expansion + minimatch 2.0.0 - 10.0.2 + Depends on vulnerable versions of brace-expansion + node_modules/minimatch + node_modules/readdir-glob/node_modules/minimatch + glob 4.3.0 - 10.5.0 + Depends on vulnerable versions of minimatch + node_modules/glob + archiver-utils >=0.2.0 + Depends on vulnerable versions of glob + node_modules/archiver-utils + node_modules/zip-stream/node_modules/archiver-utils + archiver 0.20.0 - 7.0.1 + Depends on vulnerable versions of archiver-utils + Depends on vulnerable versions of readdir-glob + Depends on vulnerable versions of zip-stream + node_modules/archiver + exceljs >=4.2.0 + Depends on vulnerable versions of archiver + node_modules/exceljs + zip-stream 0.8.0 - 6.0.1 + Depends on vulnerable versions of archiver-utils + node_modules/zip-stream + rimraf 2.3.0 - 3.0.2 || 4.2.0 - 5.0.10 + Depends on vulnerable versions of glob + node_modules/rimraf + readdir-glob <=2.0.3 + Depends on vulnerable versions of minimatch + node_modules/readdir-glob + +10 vulnerabilities (9 high, 1 critical) + +Exact installed production paths: +noderoom@0.1.1 ++-- @auth/core@0.41.1 ++-- @convex-dev/auth@0.0.94 +| `-- @auth/core@0.41.1 deduped +`-- exceljs@4.4.0 overridden + +-- archiver@5.3.2 + | +-- archiver-utils@2.1.0 + | | `-- glob@7.2.3 + | | `-- minimatch@3.1.5 + | | `-- brace-expansion@1.1.16 overridden + | +-- readdir-glob@1.1.3 + | | `-- minimatch@5.1.9 + | | `-- brace-expansion@2.1.2 + | `-- zip-stream@4.1.1 + | `-- archiver-utils@3.0.4 + | `-- glob@7.2.3 deduped + `-- unzipper@0.10.14 + `-- fstream@1.0.12 + `-- rimraf@2.7.1 + `-- glob@7.2.3 deduped diff --git a/package-lock.json b/package-lock.json index fdc24758..000af3f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@ai-sdk/openai": "^3.0.71", "@assistant-ui/react": "^0.14.14", "@assistant-ui/react-o11y": "^0.0.25", - "@auth/core": "0.41.1", + "@auth/core": "0.41.3", "@convex-dev/auth": "0.0.94", "@convex-dev/persistent-text-streaming": "^0.3.2", "@convex-dev/prosemirror-sync": "^0.2.4", @@ -53,6 +53,7 @@ "convex": "^1.42.3", "embla-carousel-react": "^8.6.0", "exceljs": "^4.4.0", + "fstream": "file:vendor/exceljs-security/fstream", "jszip": "^3.10.1", "lucide-react": "^0.515.0", "motion": "^12.42.2", @@ -69,6 +70,7 @@ "tokenlens": "^1.3.1", "undici": "^8.4.0", "unpdf": "^1.6.2", + "unzipper": "0.10.14", "use-stick-to-bottom": "^1.1.6", "zod": "4.3.6" }, @@ -468,9 +470,9 @@ } }, "node_modules/@auth/core": { - "version": "0.41.1", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.1.tgz", - "integrity": "sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw==", + "version": "0.41.3", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.3.tgz", + "integrity": "sha512-sJ3JMHHkXMD3aOjopv7mOBTO1Ocw4b0fAEXJBz6k7YHLpYQI6C40jCUPc5fNvUKxXRXNE1/sRISA15UrwWJBTw==", "license": "ISC", "dependencies": { "@panva/hkdf": "^1.2.1", @@ -482,7 +484,7 @@ "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", - "nodemailer": "^7.0.7" + "nodemailer": "^7.0.7 || ^8.0.5" }, "peerDependenciesMeta": { "@simplewebauthn/browser": { @@ -7133,75 +7135,6 @@ "react-dom": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "license": "MIT", - "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "license": "MIT", - "dependencies": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/archiver-utils/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/archiver-utils/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/archiver-utils/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/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -7297,12 +7230,6 @@ "node": ">=4" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, "node_modules/async-channel": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/async-channel/-/async-channel-0.2.0.tgz", @@ -7326,32 +7253,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { "version": "2.10.34", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz", @@ -7387,17 +7288,6 @@ "node": "*" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", @@ -7438,39 +7328,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -7700,27 +7557,6 @@ "node": ">=18" } }, - "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -7838,31 +7674,6 @@ "layout-base": "^1.0.0" } }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "license": "MIT", - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8754,15 +8565,6 @@ "embla-carousel": "8.6.0" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.24.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", @@ -9054,6 +8856,16 @@ "node": ">=8.3.0" } }, + "node_modules/exceljs/node_modules/archiver": { + "name": "@excel.js/archiver", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@excel.js/archiver/-/archiver-0.0.5.tgz", + "integrity": "sha512-kmUyrn5+rLkHI+iEnvY3mV3Jh71s5LmSCRXBfoinSILz4jyPW+gCU/kvDfrTP2LltBiYXPCFIzP4QrhGjmzJow==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -9124,9 +8936,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ { @@ -9202,12 +9014,6 @@ } } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, "node_modules/fs-monkey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", @@ -9215,12 +9021,6 @@ "dev": true, "license": "Unlicense" }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -9237,20 +9037,8 @@ } }, "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } + "resolved": "vendor/exceljs-security/fstream", + "link": true }, "node_modules/function-bind": { "version": "1.1.2", @@ -9352,27 +9140,6 @@ "dev": true, "license": "MIT" }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "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", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -9862,26 +9629,6 @@ "postcss": "^8.1.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -9898,17 +9645,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -10334,48 +10070,6 @@ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", "license": "MIT" }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "license": "MIT", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/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/lazystream/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/lazystream/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/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -10697,30 +10391,12 @@ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "license": "MIT" - }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", "license": "MIT" }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "license": "MIT" - }, "node_modules/lodash.groupby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", @@ -10752,12 +10428,6 @@ "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", "license": "MIT" }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, "node_modules/lodash.isundefined": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", @@ -10771,12 +10441,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "license": "MIT" - }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -12010,49 +11674,6 @@ "node": ">=6" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/motion": { "version": "12.42.2", "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", @@ -12166,15 +11787,6 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -12218,15 +11830,6 @@ "node": ">=12.20.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -12339,15 +11942,6 @@ "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", "license": "MIT" }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -12477,9 +12071,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz", + "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==", "dev": true, "funding": [ { @@ -12497,7 +12091,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -12590,9 +12184,9 @@ "license": "MIT" }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -13102,36 +12696,6 @@ "node": ">= 6" } }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -13393,19 +12957,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -13904,22 +13455,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/terser": { "version": "5.48.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", @@ -15463,12 +14998,6 @@ "node": ">=8" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", @@ -15513,41 +15042,6 @@ "dev": true, "license": "ISC" }, - "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "license": "MIT", - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "license": "MIT", - "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", @@ -15595,6 +15089,10 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "vendor/exceljs-security/fstream": { + "version": "1.0.12+noderoom.fail-closed.1", + "license": "MIT" } } } diff --git a/package.json b/package.json index cd17093d..02ec8584 100644 --- a/package.json +++ b/package.json @@ -280,7 +280,7 @@ "@ai-sdk/openai": "^3.0.71", "@assistant-ui/react": "^0.14.14", "@assistant-ui/react-o11y": "^0.0.25", - "@auth/core": "0.41.1", + "@auth/core": "0.41.3", "@convex-dev/auth": "0.0.94", "@convex-dev/persistent-text-streaming": "^0.3.2", "@convex-dev/prosemirror-sync": "^0.2.4", @@ -319,6 +319,7 @@ "convex": "^1.42.3", "embla-carousel-react": "^8.6.0", "exceljs": "^4.4.0", + "fstream": "file:vendor/exceljs-security/fstream", "jszip": "^3.10.1", "lucide-react": "^0.515.0", "motion": "^12.42.2", @@ -335,6 +336,7 @@ "tokenlens": "^1.3.1", "undici": "^8.4.0", "unpdf": "^1.6.2", + "unzipper": "0.10.14", "use-stick-to-bottom": "^1.1.6", "zod": "4.3.6" }, @@ -365,15 +367,17 @@ "ws": "8.21.0" }, "overrides": { - "minimatch@3.1.5": { - "brace-expansion": "1.1.16" - }, "convex": { "esbuild": "0.28.1" }, "exceljs": { + "archiver": "npm:@excel.js/archiver@0.0.5", + "unzipper": "$unzipper", "uuid": "11.1.1" }, + "unzipper@0.10.14": { + "fstream": "$fstream" + }, "linkify-it": "5.0.2", "ws": "8.21.0" } diff --git a/tests/authEmailVerification.test.ts b/tests/authEmailVerification.test.ts index e65133ad..bd477a8e 100644 --- a/tests/authEmailVerification.test.ts +++ b/tests/authEmailVerification.test.ts @@ -8,7 +8,10 @@ import { describe("password email verification", () => { it("normalizes account email identifiers and rejects malformed values", () => { expect(normalizeAuthEmail(" Person@Example.COM ")).toBe("person@example.com"); + expect(normalizeAuthEmail(" Person@Example.COM ")).toBe("person@example.com"); expect(() => normalizeAuthEmail("not-an-email")).toThrow("invalid_email"); + expect(() => normalizeAuthEmail("victim@example.com@attacker.example")).toThrow("invalid_email"); + expect(() => normalizeAuthEmail(`${"a".repeat(600)}@example.com`)).toThrow("invalid_email"); }); it("generates an email-bound eight digit challenge", async () => { diff --git a/tests/exceljsDependencyCompatibility.test.ts b/tests/exceljsDependencyCompatibility.test.ts new file mode 100644 index 00000000..f9a05ec4 --- /dev/null +++ b/tests/exceljsDependencyCompatibility.test.ts @@ -0,0 +1,171 @@ +import ExcelJS from "exceljs"; +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname } from "node:path"; +import { PassThrough, Readable } from "node:stream"; +import { describe, expect, it } from "vitest"; + +const require = createRequire(import.meta.url); +const IO_TIMEOUT_MS = 15_000; + +type ReadSummary = { + sheetNames: string[]; + rowCount: number; + header: unknown[]; + lastRow: unknown[]; +}; + +function withTimeout(operation: Promise, label: string): Promise { + let timer: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`${label} exceeded ${IO_TIMEOUT_MS}ms`)), IO_TIMEOUT_MS); + timer.unref?.(); + }); + return Promise.race([operation, timeout]).finally(() => { + if (timer) clearTimeout(timer); + }); +} + +async function writeStreamingWorkbook(label: string, dataRows: number): Promise { + const output = new PassThrough(); + const chunks: Buffer[] = []; + output.on("data", (chunk: Buffer) => chunks.push(Buffer.from(chunk))); + + const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({ + stream: output, + useSharedStrings: true, + useStyles: true, + zip: { zlib: { level: 6 } }, + }); + const sheet = workbook.addWorksheet("Credit model"); + sheet.addRow(["Facility", "Current", "Prior", "Change"]).commit(); + + for (let index = 0; index < dataRows; index += 1) { + const rowNumber = index + 2; + const row = sheet.addRow([ + `${label} - borrower ${index}`, + 1_000_000 + index, + 900_000 + index, + { formula: `B${rowNumber}-C${rowNumber}`, result: 100_000 }, + ]); + row.getCell(2).numFmt = "$#,##0"; + row.getCell(3).numFmt = "$#,##0"; + row.getCell(4).numFmt = "$#,##0"; + row.commit(); + } + + await withTimeout(workbook.commit(), `streaming XLSX write (${label})`); + return Buffer.concat(chunks); +} + +async function readStreamingWorkbook(buffer: Buffer): Promise { + const reader = new ExcelJS.stream.xlsx.WorkbookReader(Readable.from(buffer), { + worksheets: "emit", + sharedStrings: "cache", + styles: "cache", + hyperlinks: "ignore", + entries: "ignore", + }); + const summary: ReadSummary = { sheetNames: [], rowCount: 0, header: [], lastRow: [] }; + + for await (const sheet of reader) { + summary.sheetNames.push((sheet as unknown as { name: string }).name); + for await (const row of sheet) { + summary.rowCount += 1; + const values = Array.from(row.values as unknown[]); + if (row.number === 1) summary.header = values; + summary.lastRow = values; + } + } + + return summary; +} + +async function readLoadedWorkbook(buffer: Buffer): Promise { + const workbook = new ExcelJS.Workbook(); + await withTimeout(workbook.xlsx.load(Uint8Array.from(buffer).buffer), "loaded XLSX read"); + const sheet = workbook.getWorksheet("Credit model"); + if (!sheet) throw new Error("Credit model worksheet is missing"); + + return { + sheetNames: workbook.worksheets.map((worksheet) => worksheet.name), + rowCount: sheet.rowCount, + header: Array.from(sheet.getRow(1).values as unknown[]), + lastRow: Array.from(sheet.getRow(sheet.rowCount).values as unknown[]), + }; +} + +describe("ExcelJS security dependency compatibility", () => { + it("streams a sustained finance workbook through the hardened archive writer and reader", async () => { + const dataRows = 1_024; + const workbook = await writeStreamingWorkbook("sustained", dataRows); + const summary = await withTimeout(readStreamingWorkbook(workbook), "streaming XLSX read"); + + expect(workbook.subarray(0, 4).toString("hex")).toBe("504b0304"); + expect(summary.sheetNames).toEqual(["Credit model"]); + expect(summary.rowCount).toBe(dataRows + 1); + expect(summary.header.slice(1)).toEqual(["Facility", "Current", "Prior", "Change"]); + expect(summary.lastRow[1]).toBe(`sustained - borrower ${dataRows - 1}`); + expect(summary.lastRow[4]).toMatchObject({ + formula: `B${dataRows + 1}-C${dataRows + 1}`, + result: 100_000, + }); + }); + + it("keeps concurrent workbook writes isolated across sustained application-reader bursts", async () => { + for (let wave = 0; wave < 16; wave += 1) { + const labels = Array.from({ length: 4 }, (_, index) => `wave-${wave}-stream-${index}`); + const workbooks = await Promise.all( + labels.map((label) => writeStreamingWorkbook(label, 64)), + ); + const summaries = await Promise.all( + workbooks.map((workbook) => readLoadedWorkbook(workbook)), + ); + + summaries.forEach((summary, index) => { + expect(summary.lastRow[1]).toBe(`${labels[index]} - borrower 63`); + }); + expect(new Set(workbooks.map((workbook) => workbook.toString("base64"))).size).toBe(4); + } + }); + + it("fails closed and within budget when a workbook stream is truncated", async () => { + const workbook = await writeStreamingWorkbook("truncated", 32); + const truncated = workbook.subarray(0, Math.floor(workbook.length / 2)); + + await expect( + withTimeout(readStreamingWorkbook(truncated), "truncated XLSX read"), + ).rejects.toThrow(); + }); + + it("resolves the pinned archive writer and the parse-only extraction boundary", () => { + const excelRoot = dirname(require.resolve("exceljs/package.json")); + const archiver = JSON.parse( + readFileSync(require.resolve("archiver/package.json", { paths: [excelRoot] }), "utf8"), + ) as { name: string; version: string }; + const unzipperPackagePath = require.resolve("unzipper/package.json", { paths: [excelRoot] }); + const unzipperRoot = dirname(unzipperPackagePath); + const unzipper = JSON.parse(readFileSync(unzipperPackagePath, "utf8")) as { + name: string; + version: string; + }; + const fstreamPackagePath = require.resolve("fstream/package.json", { paths: [unzipperRoot] }); + const fstream = JSON.parse(readFileSync(fstreamPackagePath, "utf8")) as { + name: string; + version: string; + }; + const Fstream = require(require.resolve("fstream", { paths: [unzipperRoot] })) as { + Reader: new () => unknown; + }; + + expect(archiver).toMatchObject({ name: "@excel.js/archiver", version: "0.0.5" }); + expect(unzipper).toMatchObject({ name: "unzipper", version: "0.10.14" }); + expect(fstream).toMatchObject({ + name: "fstream", + version: "1.0.12+noderoom.fail-closed.1", + }); + expect(() => new Fstream.Reader()).toThrow( + "fstream extraction is unavailable: NodeRoom permits ExcelJS streaming parse only", + ); + }); +}); diff --git a/vendor/exceljs-security/fstream/index.cjs b/vendor/exceljs-security/fstream/index.cjs new file mode 100644 index 00000000..e2cfc81d --- /dev/null +++ b/vendor/exceljs-security/fstream/index.cjs @@ -0,0 +1,13 @@ +"use strict"; + +class UnsupportedFstreamOperation { + constructor() { + throw new Error( + "fstream extraction is unavailable: NodeRoom permits ExcelJS streaming parse only", + ); + } +} + +module.exports = UnsupportedFstreamOperation; +module.exports.Reader = UnsupportedFstreamOperation; +module.exports.Writer = UnsupportedFstreamOperation; diff --git a/vendor/exceljs-security/fstream/package.json b/vendor/exceljs-security/fstream/package.json new file mode 100644 index 00000000..90725030 --- /dev/null +++ b/vendor/exceljs-security/fstream/package.json @@ -0,0 +1,10 @@ +{ + "name": "fstream", + "version": "1.0.12+noderoom.fail-closed.1", + "description": "Fail-closed fstream compatibility boundary for ExcelJS's parse-only unzipper usage.", + "license": "MIT", + "main": "index.cjs", + "files": [ + "index.cjs" + ] +}