diff --git a/Dockerfile b/Dockerfile index db86834e2..b09dba475 100644 --- a/Dockerfile +++ b/Dockerfile @@ -154,7 +154,7 @@ RUN node -e "require.resolve('pg-boss'); require.resolve('pg')" # before the platform package, so it decides which binary runs. # # Hoist the version the app pins, by name, exactly like pdfjs-dist below. The -# store holds a second, older canvas that pdf-parse's own pdfjs-dist pulls in, +# store once held a second, older canvas that a second pdfjs-dist pulled in, # and this step used to take whichever `find | head -1` returned first. It # returned the old binary, which then sat next to the new loader: rendering # still worked, but `loadImage` called an `Image.decode` the old binary does @@ -190,13 +190,15 @@ RUN set -e; \ # `import('pdfjs-dist/legacy/build/pdf.mjs')`. Node resolves that up to # /app/node_modules, so hoist the real pnpm-store copy to the top level too. # -# Hoist the version the app pins, by name. More than one pdfjs-dist reaches the -# store — `pdf-parse` carries its own, older copy for the text-extraction path — -# and this step used to take whatever `find | head -1` returned first. That is -# readdir order, so it could hoist the OLDER copy and the version named in -# package.json would never run in the image: the renderer resolving one version -# while every local check exercised another. Name the version and fail the build -# when it is absent, instead of degrading to whichever copy comes back first. +# Hoist the version the app pins, by name. More than one pdfjs-dist once reached +# the store (a text-extraction dependency carried its own, older copy), and this +# step used to take whatever `find | head -1` returned first. That is readdir +# order, so it could hoist the OLDER copy and the version named in package.json +# would never run in the image: the renderer resolving one version while every +# local check exercised another. Name the version and fail the build when it is +# absent, instead of degrading to whichever copy comes back first. Two copies in +# one process also break each other (#1277); the lockfile now resolves one, and +# `src/__tests__/single-pdfjs-guard.test.ts` keeps it that way. COPY --from=builder /app/.pdfjs-version /tmp/.pdfjs-version RUN set -e; \ PDFJS_VERSION="$(cat /tmp/.pdfjs-version)"; \ diff --git a/next.config.ts b/next.config.ts index 055b1dcf7..cbefed2c1 100644 --- a/next.config.ts +++ b/next.config.ts @@ -302,15 +302,15 @@ const nextConfig: NextConfig = { outputFileTracingIncludes: { "*": [ "./src/lib/ai/prompts/safety-contracts.*.yaml", - // Document AI local text extraction runs `pdf-parse` (→ pdfjs-dist) - // server-side in the content-index job — the provider-free fallback that - // keeps content search working with no AI configured. Both are pure JS + - // wasm; there is NO native `.node` on the text-layer path (`@napi-rs/canvas` - // is only pulled for image rendering, which `getText()` never invokes). - // The standalone tracer can miss pdfjs-dist's wasm decoders (they resolve - // via `new URL(..., import.meta.url)`), so pin both packages into the - // runtime image explicitly. pnpm keeps pdfjs-dist unhoisted under `.pnpm`. - "./node_modules/.pnpm/pdf-parse@*/node_modules/pdf-parse/**", + // Document AI local text extraction and rasterization both run the + // app's one pdfjs-dist (`src/lib/documents/pdfjs.ts`) server-side; the + // text read is the provider-free fallback that keeps content search + // working with no AI configured. The standalone tracer can miss + // pdfjs-dist's wasm decoders, standard fonts and CMaps (they are read + // from disk at runtime), so pin the package into the runtime image + // explicitly. pnpm keeps it unhoisted under `.pnpm`. The glob matches + // one copy only: `src/__tests__/single-pdfjs-guard.test.ts` fails when + // the lockfile resolves a second version (#1277). "./node_modules/.pnpm/pdfjs-dist@*/node_modules/pdfjs-dist/**", // Document AI rasterization (`src/lib/documents/rasterize-pdf.ts`) renders // a scanned/image-only PDF page to a JPEG via `@napi-rs/canvas` so an diff --git a/package.json b/package.json index 1d968ef8d..2eed08de7 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,6 @@ "fast-check": "4.10.2", "knip": "^6.42.0", "lefthook": "^2.2.2", - "pdf-parse": "^2.4.5", "pdfjs-dist": "6.4.299", "prettier": "^3.9.10", "prettier-plugin-tailwindcss": "^0.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a47e436fa..914dac854 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,7 +26,6 @@ overrides: minimatch@<3.1.4: ^3.1.4 ip-address@<10.7.4: ^10.7.4 sharp@<0.35.0: ^0.35.3 - pdfjs-dist@>=5.6.83 <6.2.108: 6.2.108 deepmerge-ts@<8.0.0: ^8.0.0 valibot@<1.4.2: ^1.4.2 '@humanfs/node@<0.16.8': ^0.16.8 @@ -265,9 +264,6 @@ importers: lefthook: specifier: ^2.2.2 version: 2.2.2 - pdf-parse: - specifier: ^2.4.5 - version: 2.4.5 pdfjs-dist: specifier: 6.4.299 version: 6.4.299 @@ -1241,61 +1237,30 @@ packages: resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} engines: {node: '>=18'} - '@napi-rs/canvas-android-arm64@0.1.80': - resolution: {integrity: sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - '@napi-rs/canvas-android-arm64@1.0.11': resolution: {integrity: sha512-dtJS4FQEfH9OwSYLtMrEnj7T8DzBbJc/97LI/OkiWOYpW/iW5t6R/gcxVrUXS8l45gCVpwMufgucRe/pe0ISqA==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@napi-rs/canvas-darwin-arm64@0.1.80': - resolution: {integrity: sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - '@napi-rs/canvas-darwin-arm64@1.0.11': resolution: {integrity: sha512-J/JylWoJTG8OQqZaiqly4IODyAxNofUlZo0wtSBJtHk6AdymbdIkPynAM1JvQaZpPaG5h+msk9QuK8y4i1ooqw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@napi-rs/canvas-darwin-x64@0.1.80': - resolution: {integrity: sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - '@napi-rs/canvas-darwin-x64@1.0.11': resolution: {integrity: sha512-nlecCk3xy06eAGC7krZtzNDm9UNTHKuHQYK8ZYegdjzgSL8anGt/eLJZm86OWaNgZkwQwvDqfSmG8Nqe6MPZNQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.80': - resolution: {integrity: sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - '@napi-rs/canvas-linux-arm-gnueabihf@1.0.11': resolution: {integrity: sha512-DKImmG+sTPolE0kGPsYPBxTtOjb3INALp32O2lW2CfSr0bAzSEKKuu+GrQbtilClyoPOahUErS3OrznFDraoWA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@napi-rs/canvas-linux-arm64-gnu@0.1.80': - resolution: {integrity: sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@napi-rs/canvas-linux-arm64-gnu@1.0.11': resolution: {integrity: sha512-55sykACGENj5j1rhj+81AO8JbR9HYgGjRZ7/H+pNr4VE1DvINKYNMTcutk8ZmWxk276jAQvUKJ3+Kdj9GXU3lQ==} engines: {node: '>= 10'} @@ -1303,13 +1268,6 @@ packages: os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-arm64-musl@0.1.80': - resolution: {integrity: sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - '@napi-rs/canvas-linux-arm64-musl@1.0.11': resolution: {integrity: sha512-6Jz6jys17yXydO+WdJgu7JhoB4K7/w9sWHByzzTmLjQghUx3TDMWvLv+UcCdnWhg4oprET67EMymnrETdeGNAg==} engines: {node: '>= 10'} @@ -1317,13 +1275,6 @@ packages: os: [linux] libc: [musl] - '@napi-rs/canvas-linux-riscv64-gnu@0.1.80': - resolution: {integrity: sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - libc: [glibc] - '@napi-rs/canvas-linux-riscv64-gnu@1.0.11': resolution: {integrity: sha512-lf2XeEebWiUYVOVEjLXxF7dfXrBBSEOBn9AqBh39VYXrPt5cDqI+1t58IFSjn8npqkg5jIOs8TQbjKABbFW1gQ==} engines: {node: '>= 10'} @@ -1331,13 +1282,6 @@ packages: os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-x64-gnu@0.1.80': - resolution: {integrity: sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - '@napi-rs/canvas-linux-x64-gnu@1.0.11': resolution: {integrity: sha512-iDapw7nFSE5tsmQxcH2+KotJhyfVPmHd6vUOQN/Em+8WNbkZJwJ/rDowlJMFnMl0SYSDSfgKCNUGi6Sn5++Smg==} engines: {node: '>= 10'} @@ -1345,13 +1289,6 @@ packages: os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-x64-musl@0.1.80': - resolution: {integrity: sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - '@napi-rs/canvas-linux-x64-musl@1.0.11': resolution: {integrity: sha512-26q26AyITo3CGZX4HWFIMgBMA1lu3YN07gxzBm4tW6Hglq/OVyTP50kDrmWuKybCX6dh3m8Ly+c7YhI+sqCgCQ==} engines: {node: '>= 10'} @@ -1365,22 +1302,12 @@ packages: cpu: [arm64] os: [win32] - '@napi-rs/canvas-win32-x64-msvc@0.1.80': - resolution: {integrity: sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - '@napi-rs/canvas-win32-x64-msvc@1.0.11': resolution: {integrity: sha512-DFYp/ZQRdKunI/LPc0UkRXG1vlNjJKOJf8O8Ctb8+wm2Nt2ELg9Eamc31D2VoakkfGo8L+z0Iejz6WtFbQwyGw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@napi-rs/canvas@0.1.80': - resolution: {integrity: sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==} - engines: {node: '>= 10'} - '@napi-rs/canvas@1.0.11': resolution: {integrity: sha512-vt5tog1cr+QKadIddGe+3LrcX0EmjEd0izF717Uwf92mBmgYfq2XRmL2r4bZU40spB69exxhrBn9I91HYEr/7w==} engines: {node: '>= 10'} @@ -5909,15 +5836,6 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pdf-parse@2.4.5: - resolution: {integrity: sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==} - engines: {node: '>=20.16.0 <21 || >=22.3.0'} - hasBin: true - - pdfjs-dist@5.4.296: - resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==} - engines: {node: '>=20.16.0 || >=22.3.0'} - pdfjs-dist@6.4.299: resolution: {integrity: sha512-AVl138zALtfaAPvADulE0PZThbYzCBS79nL4pOSL/6Sm/4AH5A21BD9VHt97OlCuzJuCpmeZtAtkinisF4Vb1g==} engines: {node: '>=22.13.0 || >=24'} @@ -8471,82 +8389,39 @@ snapshots: strict-event-emitter: 0.5.1 optional: true - '@napi-rs/canvas-android-arm64@0.1.80': - optional: true - '@napi-rs/canvas-android-arm64@1.0.11': optional: true - '@napi-rs/canvas-darwin-arm64@0.1.80': - optional: true - '@napi-rs/canvas-darwin-arm64@1.0.11': optional: true - '@napi-rs/canvas-darwin-x64@0.1.80': - optional: true - '@napi-rs/canvas-darwin-x64@1.0.11': optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.80': - optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@1.0.11': optional: true - '@napi-rs/canvas-linux-arm64-gnu@0.1.80': - optional: true - '@napi-rs/canvas-linux-arm64-gnu@1.0.11': optional: true - '@napi-rs/canvas-linux-arm64-musl@0.1.80': - optional: true - '@napi-rs/canvas-linux-arm64-musl@1.0.11': optional: true - '@napi-rs/canvas-linux-riscv64-gnu@0.1.80': - optional: true - '@napi-rs/canvas-linux-riscv64-gnu@1.0.11': optional: true - '@napi-rs/canvas-linux-x64-gnu@0.1.80': - optional: true - '@napi-rs/canvas-linux-x64-gnu@1.0.11': optional: true - '@napi-rs/canvas-linux-x64-musl@0.1.80': - optional: true - '@napi-rs/canvas-linux-x64-musl@1.0.11': optional: true '@napi-rs/canvas-win32-arm64-msvc@1.0.11': optional: true - '@napi-rs/canvas-win32-x64-msvc@0.1.80': - optional: true - '@napi-rs/canvas-win32-x64-msvc@1.0.11': optional: true - '@napi-rs/canvas@0.1.80': - optionalDependencies: - '@napi-rs/canvas-android-arm64': 0.1.80 - '@napi-rs/canvas-darwin-arm64': 0.1.80 - '@napi-rs/canvas-darwin-x64': 0.1.80 - '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.80 - '@napi-rs/canvas-linux-arm64-gnu': 0.1.80 - '@napi-rs/canvas-linux-arm64-musl': 0.1.80 - '@napi-rs/canvas-linux-riscv64-gnu': 0.1.80 - '@napi-rs/canvas-linux-x64-gnu': 0.1.80 - '@napi-rs/canvas-linux-x64-musl': 0.1.80 - '@napi-rs/canvas-win32-x64-msvc': 0.1.80 - '@napi-rs/canvas@1.0.11': optionalDependencies: '@napi-rs/canvas-android-arm64': 1.0.11 @@ -13236,15 +13111,6 @@ snapshots: pathe@2.0.3: {} - pdf-parse@2.4.5: - dependencies: - '@napi-rs/canvas': 0.1.80 - pdfjs-dist: 5.4.296 - - pdfjs-dist@5.4.296: - optionalDependencies: - '@napi-rs/canvas': 0.1.80 - pdfjs-dist@6.4.299: optionalDependencies: '@napi-rs/canvas': 1.0.11 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4c68bc572..fc7937736 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -72,14 +72,6 @@ overrides: # (a ReDoS in the Address6 zone identifier, through 10.7.3). "ip-address@<10.7.4": "^10.7.4" "sharp@<0.35.0": "^0.35.3" - # A crafted PDF could run arbitrary JavaScript through the renderer. Our own - # copy is pinned past it in `devDependencies`, but a second copy arrives - # transitively, and `outputFileTracingIncludes` ships EVERY `pdfjs-dist@*` in - # the store into the standalone image — so an unused transitive copy is still - # a copy in the runtime image. The range is bounded on both ends deliberately: - # `pdf-parse` resolves 5.4.296, which predates the flaw and drives the local - # text-extraction path, so it must not be dragged onto a new major. - "pdfjs-dist@>=5.6.83 <6.2.108": "6.2.108" # Deeply nested input drives DeepmergeTS into unbounded recursion until the # stack gives out. The copy here is Prisma's: `@prisma/config` still asks for # 7.1.5 even on the current 7.9.1, so no Prisma bump reaches it and the diff --git a/src/__tests__/canvas-binary-hoist-guard.test.ts b/src/__tests__/canvas-binary-hoist-guard.test.ts index d230fab26..45bcc3d85 100644 --- a/src/__tests__/canvas-binary-hoist-guard.test.ts +++ b/src/__tests__/canvas-binary-hoist-guard.test.ts @@ -4,13 +4,14 @@ * * ## The failure this freezes (GitHub #1124) * - * The pnpm store holds two canvas versions: the one in package.json and an - * older one that pdf-parse's own pdfjs-dist pulls in. The runner stage picked - * the musl binary with `find | head -1`, got the old one, and copied it into - * the new loader's directory, where the loader looks first. PDF rendering - * still worked, so nothing looked wrong; `loadImage` called an - * `Image.decode` the old binary lacks, and every preview thumbnail failed - * with "image.decode is not a function" on both architectures. + * The pnpm store held two canvas versions: the one in package.json and an + * older one that a second, older pdfjs-dist pulled in (gone since #1277). + * The runner stage picked the musl binary with `find | head -1`, got the + * old one, and copied it into the new loader's directory, where the loader + * looks first. PDF rendering still worked, so nothing looked wrong; + * `loadImage` called an `Image.decode` the old binary lacks, and every + * preview thumbnail failed with "image.decode is not a function" on both + * architectures. * * ## What it proves and what it does not *