Skip to content

fix(sea-builder): verify downloaded Node.js archives before caching - #72

Merged
kurone-kito merged 3 commits into
mainfrom
issue/57-sea-builder-downloads-node-js-archives
Aug 1, 2026
Merged

fix(sea-builder): verify downloaded Node.js archives before caching#72
kurone-kito merged 3 commits into
mainfrom
issue/57-sea-builder-downloads-node-js-archives

Conversation

@kurone-kito

@kurone-kito kurone-kito commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

downloadArchive.mts — the supply-chain download path for every SEA
binary this tool produces — performed no validation at all:

  1. A non-2xx response still has a body, so a 404 page or a proxy error
    page could be streamed into the cache under the archive's name.
  2. Nothing verified the downloaded bytes against nodejs.org's
    published SHASUMS256.txt.
  3. The stream wrote directly into the permanent cache path, so any
    interrupted or failed download left a file that
    createTaskFactory's existsSync check would treat as a valid
    cache hit forever.

Changes

  • Reject non-2xx responses (both the archive fetch and the
    SHASUMS256.txt fetch) before touching the filesystem.
  • Fetch https://nodejs.org/dist/<version>/SHASUMS256.txt — derived
    from the archive URL's own path, not the caller-supplied dest
    path, so the lookup key is correct by construction rather than by
    the two happening to match today — and verify the downloaded bytes'
    SHA-256 against it while streaming. A missing, malformed (not a
    64-character hex digest), or mismatched checksum throws; nothing is
    silently skipped, and the comparison is case-insensitive.
  • Download to a unique temporary path (pid + a random UUID, so two
    concurrent downloads of the same target never share one — cache
    downloads run with { concurrent: true } in
    createListrCacheTasks) and rename onto the final archivePath
    only after the checksum passes.
  • moveIntoPlace() treats a rename refused by an already-placed
    destination as a benign race loss rather than an error: if two
    concurrent calls target the same dest, the loser's rename can fail
    (notably on Windows, which can refuse to replace a file another
    process holds open) — but since both already checksum-verified the
    same bytes, the loser just discards its own temp file instead of
    surfacing the error. A rename failure with no existing destination
    still propagates.
  • Replaced the bare, undescribed // @ts-expect-error with a
    documented toNodeReadable() cast: fetch's response.body is
    typed against the DOM lib's ReadableStream, which isn't assignable
    to node:stream/promises' pipeline without going through
    Readable.fromWeb() and a cast to node:stream/web's
    ReadableStream type.
  • Deleted toPipelineSource.mts and its spec rather than reusing
    it, per the issue's proposed alternative: it guards on .readable,
    a property a WHATWG ReadableStream does not have, so passing
    response.body through it would throw TypeError on exactly the
    input it existed to handle. After converting through
    Readable.fromWeb(), the guard is redundant, and nothing else in
    the package imported it.

Verification

  • Added downloadArchive.spec.mts (no test file existed before):
    covers a non-2xx archive response, a non-2xx SHASUMS256.txt
    response, a missing checksum entry, a malformed (non-hex) checksum
    entry, an uppercase-hex checksum entry (accepted), a checksum
    mismatch, a mid-stream error, and an empty body — each asserting no
    file is left at the destination path — plus the success path, a
    case where dest's filename deliberately differs from the archive
    URL's filename (confirming the checksum lookup key is derived from
    the URL, not the caller-supplied destination), and a mocked-rename
    case covering both the benign-race-loss path (destination already
    placed) and the propagate-on-real-failure path.
  • Statement coverage for downloadArchive.mts: 100%, both in the
    package-local vitest run --coverage (vitest 4) and in the root
    aggregate pnpm run test report (vitest 3; was 33% before this
    change, lines 11-17 uncovered — the entire download body).
  • pnpm run lint && pnpm run build && pnpm run test all pass (68
    tests).

Notes for the reviewer (not defects)

  • N targets on the same Node version now issue N identical
    SHASUMS256.txt fetches — correct, mildly redundant; caching that
    response was out of scope for this issue.
  • The archive response is fetched before the SHASUMS256.txt fetch,
    so a SHASUMS256.txt failure leaves that first response's body
    unconsumed. Harmless (no file is written until after both fetches
    succeed), flagging so it doesn't read as an oversight.

Closes #57

Summary by CodeRabbit

  • Bug Fixes

    • Archive downloads now validate server responses and verify SHA-256 checksums before completing.
    • Failed, interrupted, or invalid downloads are cleaned up without leaving incomplete destination files.
    • Downloads are finalized atomically, improving reliability and preventing corrupted archives from being used.
  • Quality Improvements

    • Added coverage for successful downloads, HTTP failures, missing checksums, checksum mismatches, interruptions, and empty responses.

downloadArchive.mts performed no validation at all: a non-2xx response
still has a body, so an error page could be streamed into the cache
under the archive's name; nothing checked the archive against
nodejs.org's published checksum; and the stream wrote directly into
the permanent cache path, so any interrupted or failed download left a
file that createTaskFactory's existsSync check would treat as a valid
cache hit forever.

- reject non-2xx responses (both the archive and the SHASUMS256.txt
  fetch) before touching the filesystem.
- fetch https://nodejs.org/dist/<version>/SHASUMS256.txt (derived from
  the archive url, not the caller-supplied dest path) and verify the
  downloaded bytes' sha-256 against it while streaming, failing closed
  on a missing or unparseable checksum line.
- download to a temporary path and rename onto the final archivePath
  only after the checksum passes; remove the temp file on any failure
  so a failed attempt leaves no cache entry behind.

replaced the bare, undescribed `// @ts-expect-error` (working around
fetch's response.body -- typed against the dom lib's ReadableStream --
not being assignable to node:stream/promises pipeline's expected type)
with a documented toNodeReadable() cast through node:stream/web's
ReadableStream type instead.

deleted toPipelineSource.mts and its spec rather than reusing it as
the issue's proposed fix suggested as an alternative: it guards on
`.readable`, a property a whatwg ReadableStream does not have, so
passing response.body through it would throw TypeError on exactly the
input it existed for. after converting through Readable.fromWeb() the
guard is redundant, and nothing else in the package imported it.

added downloadArchive.spec.mts (0 tests existed before): covers a
non-2xx archive response, a non-2xx shasums response, a missing
checksum entry, a checksum mismatch, a mid-stream error, and an empty
body, each asserting no file is left at the destination path, plus the
success path. statement coverage for downloadArchive.mts is 100% in
both the package-local vitest 4 run and the root aggregate vitest 3
run (root's own coverage report showed 33% before this change).

closes #57
Copilot AI review requested due to automatic review settings August 1, 2026 02:21
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

downloadArchive now validates HTTP responses, retrieves and checks official SHA-256 metadata, writes through a temporary file, and renames verified archives atomically. Tests cover successful downloads, failures, interruptions, empty bodies, and cleanup. The unused toPipelineSource utility and tests were removed.

Changes

Archive integrity flow

Layer / File(s) Summary
Stream and checksum helpers
packages/sea-builder/src/utils/downloadArchive.mts
The download utility converts response streams and locates the checksum for the requested archive.
Verified atomic download
packages/sea-builder/src/utils/downloadArchive.mts, packages/sea-builder/src/utils/downloadArchive.spec.mts, cspell.config.yml
The utility validates HTTP responses, streams through SHA-256 hashing into a temporary file, verifies the checksum, renames successful files, and removes failed files. Tests cover the success and failure paths. The spelling allowlist includes unstub.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant downloadArchive
  participant NodeFetch
  participant TempFile
  participant ArchivePath
  Caller->>downloadArchive: Request archive download
  downloadArchive->>NodeFetch: Fetch archive
  NodeFetch-->>downloadArchive: Return validated response
  downloadArchive->>NodeFetch: Fetch SHASUMS256.txt
  NodeFetch-->>downloadArchive: Return checksum metadata
  downloadArchive->>TempFile: Stream archive and calculate SHA-256
  TempFile-->>downloadArchive: Provide verified temporary file
  downloadArchive->>ArchivePath: Atomically rename temporary file
  ArchivePath-->>Caller: Complete download
Loading

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #57 by adding status checks, checksum verification, atomic caching, cleanup, tests, coverage, and stream-helper removal.
Out of Scope Changes check ✅ Passed All changes support issue #57, including the cspell allowlist update required for the related implementation or tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly and concisely describes the main change: verifying downloaded Node.js archives before caching.
Description check ✅ Passed The description is detailed and covers the changes, verification results, issue objectives, tests, and coverage requirements.
✨ 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 issue/57-sea-builder-downloads-node-js-archives

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copilot AI 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.

Pull request overview

This PR hardens @kurone-kito/sea-builder’s Node.js archive download path by adding HTTP-status validation, SHA-256 verification against Node’s published SHASUMS256.txt, and atomic caching semantics to prevent permanent cache poisoning.

Changes:

  • Add streaming SHA-256 verification and temp-file + atomic rename download flow in downloadArchive.mts.
  • Add a new downloadArchive test suite covering success and multiple failure modes (ensuring no destination file is left behind).
  • Remove the unused/broken toPipelineSource helper and its spec; update cspell word list.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/sea-builder/src/utils/toPipelineSource.spec.mts Removed tests for the deleted toPipelineSource helper.
packages/sea-builder/src/utils/toPipelineSource.mts Deleted unused/broken stream helper per issue acceptance criteria.
packages/sea-builder/src/utils/downloadArchive.spec.mts Added comprehensive tests for new download validation/integrity behavior.
packages/sea-builder/src/utils/downloadArchive.mts Implemented HTTP status checks, checksum verification, and atomic cache writes.
cspell.config.yml Added “unstub” to dictionary for new test usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/sea-builder/src/utils/downloadArchive.mts Outdated
Comment thread packages/sea-builder/src/utils/downloadArchive.mts Outdated

@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/sea-builder/src/utils/downloadArchive.mts`:
- Around line 46-51: Update the atomic move logic around rename(tempPath, dest)
to handle Windows EPERM/EBUSY failures with a bounded retry before propagating
the error, while preserving cleanup of the temporary file on failure. Adjust the
downloadArchive documentation to remove or platform-scope the unconditional
“atomically move” guarantee.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d623deb7-bf13-42c9-aca2-292319a208aa

📥 Commits

Reviewing files that changed from the base of the PR and between 5355018 and 36d8203.

📒 Files selected for processing (5)
  • cspell.config.yml
  • packages/sea-builder/src/utils/downloadArchive.mts
  • packages/sea-builder/src/utils/downloadArchive.spec.mts
  • packages/sea-builder/src/utils/toPipelineSource.mts
  • packages/sea-builder/src/utils/toPipelineSource.spec.mts
💤 Files with no reviewable changes (2)
  • packages/sea-builder/src/utils/toPipelineSource.mts
  • packages/sea-builder/src/utils/toPipelineSource.spec.mts

Comment thread packages/sea-builder/src/utils/downloadArchive.mts
Adds a case where dest's basename deliberately differs from the
archive URL's basename, so the existing tests no longer pass
incidentally just because both happened to match.
address review feedback on #72:

- validate the SHASUMS256.txt token is a 64-char hex digest and
  normalize case before comparing, instead of trusting it verbatim
- give each download a unique temp path instead of one keyed only by
  pid, since createListrCacheTasks runs downloads concurrently and two
  tasks can target the same dest
- treat a rename refused by an already-placed destination as a
  benign race loss instead of an error, since windows can refuse to
  replace a file another concurrent download already placed there
@kurone-kito
kurone-kito merged commit 47e4b8c into main Aug 1, 2026
23 checks passed
@kurone-kito
kurone-kito deleted the issue/57-sea-builder-downloads-node-js-archives branch August 1, 2026 02:48
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.

sea-builder downloads Node.js archives unverified and can permanently poison its cache

2 participants