Skip to content

feat(access): ingest PDF, DOCX and the raw/_inbox doorway - #7

Merged
protonspy merged 3 commits into
mainfrom
feat/sources-files
Aug 1, 2026
Merged

feat(access): ingest PDF, DOCX and the raw/_inbox doorway#7
protonspy merged 3 commits into
mainfrom
feat/sources-files

Conversation

@protonspy

@protonspy protonspy commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Closes plan tasks 3.3, 3.4 and 3.7 of plans/open-wiki.md.

What changed

3.3 — Upload a PDF. Text is extracted one page at a time and written under a ## p<N> heading, so the fragment of src://<id>#p12 is also the markdown anchor of the heading it points at. A page with no extractable text keeps its anchor — dropping it would shift every page number after it.

3.4 — Upload a DOCX. Text and heading hierarchy, via mammoth. Its own markdown writer is deprecated and escapes ordinary prose (split in two\.), so the adapter converts mammoth's HTML subset itself. No page anchor is written: a DOCX records no pagination, and a synthetic p<N> would be a number that looks like provenance and points nowhere.

3.7 — The raw/_inbox/ doorway. Scaffolded, gitignored, and drained through the same path as an upload. It is not a source: nothing enumerates it, cites it, or reports it uncited. A file that cannot be ingested stays where it is with the reason reported — it is the user's only copy of it.

A shared ingestSource dispatcher recognises the format and picks the adapter; 3.5 will call the same door.

One thing to decide, not fixed here

A DOCX has no page anchor, but the already-shipped 5.4 accepts only p<N> for a src:// citation — so a DOCX source is citable only as src://<id>#p1, which resolves to the source but to no place inside its text.md. That is a real gap between 3.4 and 5.4. It is now recorded on the plan's 3.4 line rather than left implied by a comment in one file, and deferred to group 7 with the rest of provenance. The MVP path that matters end to end is markdown and PDF.

From the reviews on this branch

code-review and security-review were run on the diff before this PR was opened. Both found real defects; all are fixed here:

  • The watcher ingested half-written files. awaitWriteFinish stabilises the path an event names, but the drain re-read the whole directory — so a small file's event pulled in a large one still mid-copy, froze half of it as an immutable source, and deleted the user's only copy. Now an event ingests the file it named. chokidar also applies no stability check at all before it emits ready, so the guarantee is ours: each file is observed twice before it is read.
  • The doorway was confined against raw/, not the project. That asserted nothing about raw/ itself, so a symlink standing there landed bytes outside the project and still reported failure. inboxPath and registerSource both confine against the project now, before anything is created.
  • Decompression bomb. A 551 KB DOCX declaring 166 MB of XML exhausted the heap — which V8 aborts rather than throws, so no try/catch downstream contained it, and because the file only leaves the inbox on success it killed every subsequent start too. Size ceilings on both readers, and the zip's declared sizes are checked before anything inflates.
  • A new ignore rule never reached an existing project. The managed block was skipped whenever it was already present, while scaffold still created the new directory — so raw/_inbox/ appeared unignored in exactly the projects that already existed. The block is rewritten now; opting in moves to a negation below the closing marker, which git honours and the tool never rewrites.
  • An unhandled chokidar error event crashed the host process. A node EventEmitter that emits error with no listener throws.
  • DOCX conversion defects, each with a test that had been asserting markup mammoth never emits: table rows lost all structure, multi-paragraph list items became several items, nested ordered lists indented too shallow to nest, and an emphasis run carrying an edge space rendered as literal asterisks.
  • A literal NUL byte in inbox.ts made git treat the most concurrency-sensitive file on the branch as binary, with no visible diff.
  • A test that passed with the code removed. The serialisation test only inspected successes, so the refusals a race produces were filtered out; it now asserts on those.

How it was verified

  • 363 tests pass (pnpm test); access coverage 94.6% against the 76% floor
  • pnpm run typecheck and pnpm lint clean
  • scc validate — 0 findings
  • Fixtures are built in-process: a real PDF with a real xref table, and a real OPC zip, so the tests assert against a document whose content they state rather than a checked-in blob

Note on scope

main is not prettier-clean — CI runs typecheck and lint but not format:check, so ~30 pre-existing files reformat on any pnpm format. That churn was deliberately kept out of this diff. Worth a separate housekeeping PR plus a format:check step in CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_016iMM93Wk43o44V5J2AxPgL

Summary by CodeRabbit

  • New Features

    • Added PDF and DOCX importing with preserved originals and searchable extracted text.
    • Added unified uploads for text, PDF, and DOCX files.
    • Added automatic inbox processing for newly added files.
    • Added PDF page anchors and DOCX-to-Markdown conversion.
  • Bug Fixes

    • Strengthened validation for paths, symlinks, duplicates, file sizes, and malformed documents.
    • Improved reliable processing while files are still being written.
    • Ignore-file management now updates incomplete or outdated entries safely.
  • Documentation

    • Updated stack documentation and project progress tracking.

Closes plan tasks 3.3, 3.4 and 3.7.

3.3 — A PDF's text is extracted per page and written under a `## p<N>`
heading, so the fragment of `src://<id>#p12` is also the markdown anchor
of the heading it points at. A page with no extractable text keeps its
anchor: dropping it would shift every page number after it. pdfjs-dist
rather than the pdf-parse this repo's stack.md had pencilled in —
pdf-parse wraps an old fork of the same engine and returns the document
as one string, so the page boundary, the only thing a citation needs,
has to be recovered through a render hook.

3.4 — A DOCX's text and heading hierarchy, read with mammoth. Its own
markdown writer is deprecated and escapes ordinary prose (`in two\.`),
so this converts mammoth's HTML subset itself. No page anchor: the
format records no pagination, and a synthetic `p<N>` would be a number
that looks like provenance and points nowhere. That leaves a real gap
against the shipped 5.4, which accepts only `p<N>` — recorded on the
plan's 3.4 line rather than left implied by a comment.

3.7 — `raw/_inbox/` is scaffolded, gitignored and drained through the
same path as an upload. It is not a source: nothing enumerates it,
cites it, or reports it uncited. A file that cannot be ingested stays
where it is with the reason reported — it is the user's only copy.

Also from the two reviews on this branch:

- The watcher ingests the file an event named, not the whole directory.
  chokidar's awaitWriteFinish stabilises one path; re-reading everything
  on that event picked up a neighbour still mid-copy, froze half of it
  as an immutable source and deleted the original. It also applies no
  stability check at all before `ready`, so the guarantee is ours: each
  file is observed twice before it is read.
- The doorway is confined against the project, not against `raw/`, and
  registerSource confines before it creates anything. Rooting the
  assertion at `raw/` asserted nothing about `raw/` itself, so a symlink
  there landed bytes outside the project and still reported failure.
- Size ceilings on both readers. A 551 KB DOCX declaring 166 MB of XML
  exhausted the heap, which V8 aborts rather than throws, so no
  try/catch downstream contained it — and the file survived to kill the
  next start too. The zip's declared sizes are checked before anything
  inflates.
- The managed .gitignore block is rewritten rather than skipped, so a
  rule added later reaches a project scaffolded earlier. Opting in moves
  to a negation below the closing marker, which git honours and the tool
  never rewrites — something the file can state, unlike an edit inside
  the block that nothing could tell from a mistake.
- chokidar's `error` event is handled. Unhandled, an EventEmitter error
  throws and takes the host process with it.
- Table cells, multi-paragraph list items, nested ordered-list indents
  and emphasis runs carrying an edge space are all handled; the tests
  that covered them asserted markup mammoth never emits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016iMM93Wk43o44V5J2AxPgL
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The access package adds PDF and DOCX ingestion, unified upload dispatch, and a raw/_inbox ingestion doorway. It adds page-aware PDF Markdown, HTML-to-Markdown DOCX conversion, size and path validation, source persistence, filesystem watching, and related tests and exports.

Changes

Source ingestion

Layer / File(s) Summary
Upload contracts and registration
packages/access/src/sources/upload.ts, packages/access/src/sources/register.ts, packages/access/src/index.ts, packages/access/package.json, packages/access/types/mammoth.d.ts, packages/access/tests/sources-upload.spec.ts
The package recognizes text, PDF, and DOCX files, validates names and sizes, dispatches format-specific adapters, prevents unsafe registration paths, and exposes the new APIs and types.
PDF extraction and persistence
packages/access/src/sources/pdf.ts, packages/access/tests/sources-pdf.spec.ts, packages/access/tests/fixtures/documents.ts
PDF pages are extracted with page limits and cleanup, rendered with ## p<N> anchors, and stored with the original PDF and page metadata.
DOCX conversion and persistence
packages/access/src/sources/docx.ts, packages/access/types/mammoth.d.ts, packages/access/tests/sources-docx.spec.ts, packages/access/tests/fixtures/documents.ts
DOCX files undergo ZIP size validation, Mammoth HTML conversion, custom Markdown rendering, and source persistence.
Inbox lifecycle and watcher
packages/access/src/sources/inbox.ts, packages/access/src/sources/manifest.ts, packages/access/src/sources/id.ts, packages/access/src/scaffold.ts, packages/access/src/ignore.ts, packages/access/tests/sources-inbox.spec.ts, packages/access/tests/ignore.spec.ts
The scaffold creates and ignores raw/_inbox. Inbox processing validates stable regular files, removes files only after successful ingestion, reports per-file outcomes, and serializes Chokidar events and drains.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ingestSource
  participant uploadPdfSource
  participant extractPdfPages
  participant registerSource
  Client->>ingestSource: submit PDF buffer
  ingestSource->>uploadPdfSource: dispatch recognized format
  uploadPdfSource->>extractPdfPages: extract page text
  extractPdfPages-->>uploadPdfSource: return PdfPage[]
  uploadPdfSource->>registerSource: persist original and text.md
  registerSource-->>Client: return source id and page count
Loading
sequenceDiagram
  participant FileSystem
  participant watchInbox
  participant drainInbox
  participant ingestInboxEntry
  FileSystem->>watchInbox: emit stable-file event
  watchInbox->>drainInbox: queue serialized drain
  drainInbox->>ingestInboxEntry: validate and ingest entry
  ingestInboxEntry-->>drainInbox: return InboxOutcome
  drainInbox-->>watchInbox: remove success or retain refusal
  watchInbox-->>FileSystem: report outcome or error
Loading

Possibly related PRs

  • protonspy/open-wiki#6: This PR extends the same access source-ingestion modules with PDF/DOCX uploads and inbox handling.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: PDF and DOCX ingestion plus the raw/_inbox doorway.
Docstring Coverage ✅ Passed Docstring coverage is 80.95% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sources-files

Comment @coderabbitai help to get the list of available commands.

…e symlink privilege

The repo already established this in paths.spec.ts: creating a symlink is
a privilege a Windows account may not have, and that failure is a
different thing from the containment behaviour under test. Windows is the
only platform this product supports, so the suite has to run there for a
developer who is not elevated.

Deleting a directory chokidar is watching is the same kind of platform
difference, and is guarded the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016iMM93Wk43o44V5J2AxPgL

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
packages/access/src/ignore.ts (1)

21-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Build the inbox rule from the INBOX constant.

manifest.ts centralizes INBOX because a rename must not miss one copy of the string. This rule is another copy, and it is the quietest one: a rename would leave the doorway git-visible with no compile error.

♻️ Proposed refactor
+import { INBOX } from "./sources/manifest.js";
+
 const BODY = [
   ...
-  "raw/_inbox/",
+  `raw/${INBOX}/`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/access/src/ignore.ts` around lines 21 - 24, Update the ignore rule
in the exported ignore configuration to derive the inbox path from the
centralized INBOX constant in manifest.ts rather than duplicating the raw
string. Preserve the existing trailing-slash ignore behavior and explanatory
comments.
packages/access/src/sources/docx.ts (1)

358-367: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

A failure in writeSourceText leaves a registered source with no text.md.

registerSource creates raw/<id>/, writes source.docx, and writes manifest.json. If writeSourceText then throws, the id is taken and the source has no extracted text. A retry of the same filename fails with TakenIdError, so the user cannot recover without deleting the directory by hand. The realistic trigger is a disk or permission error, not a malformed document, because extraction already completed.

uploadPdfSource in packages/access/src/sources/pdf.ts follows the same sequence and carries the same gap. Consider a shared helper that removes raw/<id>/ when the text write fails.

♻️ Proposed cleanup on failure
 export async function uploadDocxSource(
   projectRoot: string,
   name: string,
   content: Buffer,
 ): Promise<{ id: string }> {
   const markdown = await extractDocxMarkdown(content);
   const { id } = registerSource(projectRoot, { name, kind: "file", content });
-  writeSourceText(projectRoot, id, markdown);
+  try {
+    writeSourceText(projectRoot, id, markdown);
+  } catch (err) {
+    // The id is frozen the moment the directory exists. A source with no
+    // `text.md` is not a source, so leave nothing behind to block a retry.
+    rmSync(join(projectRoot, "raw", id), { recursive: true, force: true });
+    throw err;
+  }
   return { id };
 }

Add the imports:

+import { rmSync } from "node:fs";
+import { join } from "node:path";
 import { registerSource } from "./register.js";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/access/src/sources/docx.ts` around lines 358 - 367, Update
uploadDocxSource and the analogous uploadPdfSource flow so a failure in
writeSourceText removes the newly registered raw/<id> directory before
rethrowing the error. Prefer a shared cleanup helper for both sources,
preserving successful uploads and allowing retries after disk or permission
failures.
packages/access/types/mammoth.d.ts (1)

22-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Narrow the declaration to the API used by sources/docx.ts.

Remove the unused extractRawText declarations. Keep export default mammoth; mammoth@1.12.0 is CommonJS, so dynamic import exposes its exports through .default.

Proposed narrowing
   export function convertToHtml(input: ConvertInput): Promise<ConvertResult>;
-  export function extractRawText(input: ConvertInput): Promise<ConvertResult>;
 
   const mammoth: {
     convertToHtml: typeof convertToHtml;
-    extractRawText: typeof extractRawText;
   };
   export default mammoth;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/access/types/mammoth.d.ts` around lines 22 - 29, In the mammoth
module declaration, remove the extractRawText function declaration and its
corresponding property from the default mammoth object. Keep convertToHtml and
preserve the export default mammoth shape so the CommonJS dynamic-import
contract remains represented through .default.
packages/access/src/sources/pdf.ts (1)

129-138: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Drop the redundant buffer copy.

Buffer is already a Uint8Array, and extractPdfPages copies the bytes again at Line 81 before handing them to pdfjs. The copy at Line 134 therefore holds a third full image of the file in memory. At the 64 MiB ceiling this is 128 MiB of avoidable allocation per upload.

♻️ Proposed refactor
-  const pages = await extractPdfPages(new Uint8Array(content));
+  const pages = await extractPdfPages(content);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/access/src/sources/pdf.ts` around lines 129 - 138, Update
uploadPdfSource to pass the existing content Buffer directly to extractPdfPages
instead of wrapping it in a new Uint8Array; preserve the remaining source
registration, text writing, and return behavior unchanged.
packages/access/src/sources/upload.ts (1)

44-48: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Use one basename rule for recognition and id derivation.

extensionOf splits on / and \. basename from node:path on POSIX does not treat \ as a separator. If a Windows-style path reaches this function on a POSIX host (a test, or a CLI invoked with a path copied from Windows), format resolves correctly but name keeps the directory components, and deriveId produces c-users-u-report.pdf. This is the same class of mismatch the comment on Lines 86-90 describes.

Extract the basename once with the same separator rule and pass it to both.

♻️ Proposed refactor
-/** The lowercased extension of a filename, `.pdf`, or `""` when it has none. */
-function extensionOf(name: string): string {
-  const base = name.split(/[\\/]/).pop() ?? name;
-  const dot = base.lastIndexOf(".");
+/** The last path component, under either separator. */
+function baseNameOf(name: string): string {
+  return name.split(/[\\/]/).pop() ?? name;
+}
+
+/** The lowercased extension of a filename, `.pdf`, or `""` when it has none. */
+function extensionOf(name: string): string {
+  const base = baseNameOf(name);
+  const dot = base.lastIndexOf(".");
   return dot > 0 ? base.slice(dot).toLowerCase() : "";
 }
-  const name = basename(rawName);
+  const name = baseNameOf(rawName);

Also applies to: 86-94

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/access/src/sources/upload.ts` around lines 44 - 48, Unify basename
extraction for format recognition and ID derivation in the upload flow. Reuse
the separator-aware basename computed by extensionOf—or extract it through a
shared helper—and pass that same basename to deriveId, ensuring Windows-style
paths do not retain directory components while preserving existing extension
detection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/access/package.json`:
- Around line 18-20: Align the repository’s root Node engine requirement with
pdfjs-dist@6.2.108 by raising the minimum from >=22 to >=22.13.0, while
preserving the existing >=24 allowance. Update the root package.json engines
configuration rather than changing the dependency selection.

In `@packages/access/src/sources/docx.ts`:
- Around line 341-348: Update extractDocxMarkdown and the
declaredUncompressedSize ZIP64 handling so ZIP64 sentinel values produce a
distinct refusal message explaining that the document uses ZIP64, rather than
interpolating Infinity as a byte count; retain the existing size-limit message
for finite declared sizes and add coverage for both ZIP64 sentinel paths.
- Around line 31-32: Update the numeric-entity replacements in htmlToMarkdown to
validate decoded values before calling String.fromCodePoint, covering both
decimal and hexadecimal entities. Preserve valid conversions, but leave entities
with values outside the Unicode range (above 0x10FFFF) as their original literal
text instead of throwing.

In `@packages/access/src/sources/inbox.ts`:
- Around line 321-327: Update the initial readiness promise in watchInbox to
settle on both watcher “ready” and “error” events. Use a shared one-time cleanup
callback that removes both listeners before resolving, preventing a fatal scan
error from leaving the watchInbox flow hanging.

In `@packages/access/tests/fixtures/documents.ts`:
- Line 1: Set the project’s Node.js minimum version to >=22.2.0 in the
engines.node configuration and update CI’s node-version setting from the broad
22 range to enforce that floor. Ensure all relevant version declarations align
so packages and CI do not run with Node.js versions where zlib.crc32 is
unavailable.

In `@packages/access/tests/sources-inbox.spec.ts`:
- Around line 30-34: Update tempProject to resolve the temporary root with
realpathSync before returning it, and import realpathSync from node:fs. Ensure
assertions comparing inboxPath(root) use the resolved path consistently,
including the additional assertion around lines 51–53.

---

Nitpick comments:
In `@packages/access/src/ignore.ts`:
- Around line 21-24: Update the ignore rule in the exported ignore configuration
to derive the inbox path from the centralized INBOX constant in manifest.ts
rather than duplicating the raw string. Preserve the existing trailing-slash
ignore behavior and explanatory comments.

In `@packages/access/src/sources/docx.ts`:
- Around line 358-367: Update uploadDocxSource and the analogous uploadPdfSource
flow so a failure in writeSourceText removes the newly registered raw/<id>
directory before rethrowing the error. Prefer a shared cleanup helper for both
sources, preserving successful uploads and allowing retries after disk or
permission failures.

In `@packages/access/src/sources/pdf.ts`:
- Around line 129-138: Update uploadPdfSource to pass the existing content
Buffer directly to extractPdfPages instead of wrapping it in a new Uint8Array;
preserve the remaining source registration, text writing, and return behavior
unchanged.

In `@packages/access/src/sources/upload.ts`:
- Around line 44-48: Unify basename extraction for format recognition and ID
derivation in the upload flow. Reuse the separator-aware basename computed by
extensionOf—or extract it through a shared helper—and pass that same basename to
deriveId, ensuring Windows-style paths do not retain directory components while
preserving existing extension detection.

In `@packages/access/types/mammoth.d.ts`:
- Around line 22-29: In the mammoth module declaration, remove the
extractRawText function declaration and its corresponding property from the
default mammoth object. Keep convertToHtml and preserve the export default
mammoth shape so the CommonJS dynamic-import contract remains represented
through .default.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ac0edf93-6b06-403b-ae84-e06d061a5581

📥 Commits

Reviewing files that changed from the base of the PR and between 26cfbff and 50ec3a6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • docs/stack.md
  • packages/access/package.json
  • packages/access/src/ignore.ts
  • packages/access/src/index.ts
  • packages/access/src/scaffold.ts
  • packages/access/src/sources/docx.ts
  • packages/access/src/sources/id.ts
  • packages/access/src/sources/inbox.ts
  • packages/access/src/sources/manifest.ts
  • packages/access/src/sources/pdf.ts
  • packages/access/src/sources/register.ts
  • packages/access/src/sources/upload.ts
  • packages/access/tests/fixtures/documents.ts
  • packages/access/tests/ignore.spec.ts
  • packages/access/tests/sources-docx.spec.ts
  • packages/access/tests/sources-inbox.spec.ts
  • packages/access/tests/sources-pdf.spec.ts
  • packages/access/tests/sources-upload.spec.ts
  • packages/access/tsconfig.json
  • packages/access/types/mammoth.d.ts
  • plans/open-wiki.md

Comment thread packages/access/package.json
Comment thread packages/access/src/sources/docx.ts Outdated
Comment thread packages/access/src/sources/docx.ts
Comment thread packages/access/src/sources/inbox.ts
@@ -0,0 +1,182 @@
import { crc32 } from "node:zlib";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find the declared Node floor and the CI Node version.
set -euo pipefail

fd -H -t f 'package.json' -E node_modules --exec rg -n -A3 '"engines"' {} \; -x echo '--- {}'
fd -H -t f -e yml -e yaml . .github --exec rg -n 'node-version|setup-node' {} \;
fd -H -t f '.nvmrc|.tool-versions' --exec cat {} \;

Repository: protonspy/open-wiki

Length of output: 238


🌐 Web query:

Which Node.js version added the zlib.crc32 API?

💡 Result:

The zlib.crc32 API was added to Node.js in versions v22.2.0 and v20.15.0 [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- package engines ---'
cat -n package.json | sed -n '1,18p'
printf '%s\n' '--- CI Node configuration ---'
if [ -d .github ]; then
  rg -n -C2 'setup-node|node-version|NODE_VERSION' .github || true
fi
printf '%s\n' '--- repository Node version files ---'
for f in .nvmrc .tool-versions; do
  if [ -f "$f" ]; then
    printf '%s\n' "--- $f"
    cat -n "$f"
  fi
done
printf '%s\n' '--- crc32 fixture usage ---'
rg -n -C3 'crc32|documents' packages/access/tests/fixtures/documents.ts

Repository: protonspy/open-wiki

Length of output: 2719


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json
from pathlib import Path

p = json.loads(Path("package.json").read_text())
print("engines:", p.get("engines"))
for path in sorted(Path(".github").rglob("*")) if Path(".github").exists() else []:
    if path.is_file() and path.suffix in {".yml", ".yaml"}:
        text = path.read_text(errors="replace")
        if "setup-node" in text or "node-version" in text:
            print(f"--- {path}")
            for i, line in enumerate(text.splitlines(), 1):
                if "setup-node" in line or "node-version" in line:
                    print(f"{i}: {line}")
PY

Repository: protonspy/open-wiki

Length of output: 530


Set the Node.js floor to >=22.2.0.

engines.node: ">=22" and CI node-version: 22 permit Node.js versions before 22.2.0, where zlib.crc32 is unavailable and DOCX fixtures fail.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/access/tests/fixtures/documents.ts` at line 1, Set the project’s
Node.js minimum version to >=22.2.0 in the engines.node configuration and update
CI’s node-version setting from the broad 22 range to enforce that floor. Ensure
all relevant version declarations align so packages and CI do not run with
Node.js versions where zlib.crc32 is unavailable.

Comment thread packages/access/tests/sources-inbox.spec.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/access/tests/sources-inbox.spec.ts`:
- Around line 52-58: In packages/access/tests/sources-inbox.spec.ts:52-58,
update trySymlink to suppress only the expected Windows symlink-privilege error
and rethrow all other filesystem errors; in
packages/access/tests/sources-inbox.spec.ts:350-356, apply the same narrowing to
the cleanup catch so only the known watched-directory deletion error is ignored,
while unexpected errors propagate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d27f1a1e-2376-4938-a76f-1a4cedb9dc90

📥 Commits

Reviewing files that changed from the base of the PR and between 50ec3a6 and 39ef209.

📒 Files selected for processing (1)
  • packages/access/tests/sources-inbox.spec.ts

Comment thread packages/access/tests/sources-inbox.spec.ts
- `watchInbox` no longer hangs when chokidar never emits `ready`. The
  event is not guaranteed after a failure in the initial scan, and a bare
  await on it means the desktop application never finishes opening the
  project. It now settles on ready or error, whichever comes first, with
  a bounded fallback.
- A ZIP64 DOCX is reported as ZIP64. It was refused correctly and then
  described to the user as declaring "Infinity bytes of content".
- A numeric character reference above the Unicode range no longer aborts
  the conversion. `String.fromCodePoint` throws there, and
  `htmlToMarkdown` is exported, so the HTML need not have come from
  mammoth. Out-of-range and surrogate-half references stay literal.
- `engines.node` is `>=22.13.0`: pdfjs-dist 6.2.108 requires it, and
  `zlib.crc32` — which the DOCX fixture builds on — landed in 22.2.0.
- The inbox tests resolve their temp root. `inboxPath` returns a real
  path, and `os.tmpdir()` is itself a symlink on macOS, so the assertion
  compared two spellings of the same directory.
- The two platform skips catch only the codes that mean "this platform
  cannot do that". Catching everything turned an unexpected setup
  failure into a passing test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016iMM93Wk43o44V5J2AxPgL

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/access/src/sources/docx.ts`:
- Around line 331-341: Update the ZIP metadata validation around the EOCD
parsing and central-directory loop to reject ZIP64 whenever either EOCD
entry-count field, central-directory-size field, or offset uses its sentinel,
and reject any central-directory entry whose compressed or uncompressed size is
ZIP64_SENTINEL. Preserve the existing archive-size limit behavior, and add
coverage for an EOCD entry-count sentinel that would otherwise omit a later
oversized entry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d8f6b354-71fa-48b7-bcd3-82620fa50172

📥 Commits

Reviewing files that changed from the base of the PR and between 39ef209 and 9928812.

📒 Files selected for processing (6)
  • package.json
  • packages/access/src/sources/docx.ts
  • packages/access/src/sources/inbox.ts
  • packages/access/tests/fixtures/documents.ts
  • packages/access/tests/sources-docx.spec.ts
  • packages/access/tests/sources-inbox.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/access/tests/sources-docx.spec.ts
  • packages/access/tests/fixtures/documents.ts
  • packages/access/src/sources/inbox.ts

Comment on lines +331 to +341
if (offset === ZIP64_SENTINEL) return ZIP64;

let total = 0;
for (let n = 0; n < entries; n++) {
if (offset + 46 > zip.length) return null;
if (zip.readUInt32LE(offset) !== CENTRAL_SIGNATURE) return null;
const size = zip.readUInt32LE(offset + 24);
// A ZIP64 entry hides its real size in an extra field. Rather than parse
// that, say so: a document that needs ZIP64 is not one this reads, and
// "declares Infinity bytes" is not a sentence to show anybody.
if (size === ZIP64_SENTINEL) return ZIP64;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Reject all ZIP64 EOCD sentinel fields.

A ZIP64 archive can set the EOCD entry count to 0xffff while keeping offset below 0xffffffff. The loop then reads only 65,535 entries and can omit a later oversized entry from total. This bypasses the 64 MiB pre-extraction limit before Mammoth reads the archive.

Reject ZIP64 when any EOCD count or central-directory-size field uses its sentinel. Also reject a central-directory entry when its compressed or uncompressed size uses the ZIP64 sentinel. Add a test with a ZIP64 entry-count sentinel and an oversized omitted entry.

Proposed fix
+  const entriesOnThisDisk = zip.readUInt16LE(eocd + 8);
   const entries = zip.readUInt16LE(eocd + 10);
+  const centralDirectorySize = zip.readUInt32LE(eocd + 12);
   let offset = zip.readUInt32LE(eocd + 16);
-  if (offset === ZIP64_SENTINEL) return ZIP64;
+  if (
+    entriesOnThisDisk === 0xffff ||
+    entries === 0xffff ||
+    centralDirectorySize === ZIP64_SENTINEL ||
+    offset === ZIP64_SENTINEL
+  ) {
+    return ZIP64;
+  }

   let total = 0;
   for (let n = 0; n < entries; n++) {
     if (offset + 46 > zip.length) return null;
     if (zip.readUInt32LE(offset) !== CENTRAL_SIGNATURE) return null;
+    const compressedSize = zip.readUInt32LE(offset + 20);
     const size = zip.readUInt32LE(offset + 24);
-    if (size === ZIP64_SENTINEL) return ZIP64;
+    if (compressedSize === ZIP64_SENTINEL || size === ZIP64_SENTINEL) return ZIP64;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/access/src/sources/docx.ts` around lines 331 - 341, Update the ZIP
metadata validation around the EOCD parsing and central-directory loop to reject
ZIP64 whenever either EOCD entry-count field, central-directory-size field, or
offset uses its sentinel, and reject any central-directory entry whose
compressed or uncompressed size is ZIP64_SENTINEL. Preserve the existing
archive-size limit behavior, and add coverage for an EOCD entry-count sentinel
that would otherwise omit a later oversized entry.

@protonspy
protonspy merged commit 1f28659 into main Aug 1, 2026
8 checks passed
@protonspy
protonspy deleted the feat/sources-files branch August 1, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant