Guard streamTagFinal() against being made eager
All checks were successful
check / check (push) Successful in 4s

Rework against review finding B-A on the branch.

The claim was false as written. Commit 8a200be's message, the PR body and
the comments on both sides said the repurposed pinning test in
test/crypto/stream.test.ts fails if streamTagFinal() is ever made eager.
It does not: substituting

    const EAGER: number = sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;
    export const streamTagFinal = (): number => EAGER;

leaves the whole suite green. Under vitest, sodium is already initialised
in the worker process by the time a source module is evaluated, so an
eager read picks up a real value there and value equality cannot see the
difference. The danger it claimed to cover is real: a direct probe of the
vendored libsodium-wrappers-sumo gives undefined before await sodium.ready
and 3 after, so an eager read would ship a library that rejects every
valid download as truncated for anyone importing it in plain Node ESM,
while make check stayed green.

Rather than drop the claim, this makes it true. A new test reproduces the
plain-Node ESM ordering that vitest hides: vi.resetModules() plus a
doMock'd stand-in sodium whose TAG_FINAL property is absent while
src/crypto/stream.ts is evaluated and appears only afterwards, exactly as
libsodium attaches its constants inside ready.then(...). A call-time read
sees the value that appeared after evaluation; a module-level read binds
undefined and the test fails. The stand-in hands back a sentinel rather
than 3, so a read that somehow reached the real library, or a return to a
hardcoded literal, fails too.

Demonstrated rather than argued: with the two-line eager variant above in
place, make test reports 1 failed | 140 passed, "expected undefined to be
42", exit 2. Restored with git checkout -- src/crypto/stream.ts, make test
reports 141 passed. The eager variant was applied with the editor and
reverted with git, not by scripted substitution.

The pinning test keeps its original job — value equality against the tag
observed on a real final chunk — and its doc comment now says only that.
The comment on the accessor in src/crypto/stream.ts names the guard test,
and records why the ordinary tests cannot see the bug on their own.
This commit was merged in pull request #20.
This commit is contained in:
2026-08-09 02:45:35 +00:00
parent 8a200be8a7
commit 937bcb7aee
2 changed files with 67 additions and 9 deletions

View File

@@ -12,11 +12,20 @@ export const STREAM_CHUNK_OVERHEAD = 17;
// marks the last chunk of a stream. Exported so callers (the download layer)
// can detect truncation without importing sodium themselves.
//
// This is a function rather than a constant because libsodium attaches its
// constants to the module object only once `sodium.ready` has resolved, which
// is long after this module is evaluated — a module-level read would bind
// `undefined`. Reading it at call time returns the library's own value, so
// there is no second copy of a protocol constant to keep in sync.
// This is a function rather than a constant, and that is load-bearing:
// libsodium attaches its constants to the module object inside
// `ready.then(...)`, which resolves long after this module is evaluated. A
// module-level read would bind `undefined`, every downstream comparison
// against it would then be false, and every valid download would be rejected
// as truncated. Reading at call time returns the library's own value, so
// there is also no second copy of a protocol constant to keep in sync.
//
// Under vitest sodium is already initialised in the worker before this module
// is evaluated, so an eager read would pick up a real value there and the
// ordinary tests could not tell the difference. The regression guard is
// "streamTagFinal() reads the constant at call time, not at import time" in
// test/crypto/stream.test.ts, which reproduces the plain-Node ESM ordering
// against a stand-in sodium module; it goes red if this becomes eager.
export const streamTagFinal = (): number =>
sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;