Skip to content

chore: deterministic fuzz, DRY chunk verify, DRY KDF metadata#15

Open
flyingrobots wants to merge 2 commits intomainfrom
chore/followups
Open

chore: deterministic fuzz, DRY chunk verify, DRY KDF metadata#15
flyingrobots wants to merge 2 commits intomainfrom
chore/followups

Conversation

@flyingrobots
Copy link
Member

@flyingrobots flyingrobots commented Feb 28, 2026

Summary

Three hygiene fixes from architecture audit — no public API changes, no new features:

  • Deterministic fuzz test — envelope fuzz round-trip uses seeded xorshift32 PRNG instead of Math.random(), making failures reproducible
  • DRY chunk verification — extracted _readAndVerifyChunk() in CasService; buffered and streaming restore paths share one implementation
  • DRY KDF metadata — extracted buildKdfMetadata() helper; VaultService and ContentAddressableStore both call it instead of duplicating the KDF object spread

Test plan

  • npx eslint . — 0 errors
  • npm test — 788 tests pass (was 709 at last release; +3 new buildKdfMetadata tests)
  • Bun unit tests
  • Deno unit tests

Summary by CodeRabbit

  • Tests

    • Deterministic fuzz tests now use seeded randomness for reproducible failures.
    • Added comprehensive unit tests for KDF metadata construction.
  • Refactor

    • Consolidated chunk integrity verification across all restore paths.
    • Extracted KDF metadata construction into a reusable helper.
    • Extracted key-resolution logic into a dedicated KeyResolver service and unified crypto adapter sha256 to be consistently async.
  • Chores

    • Added a code-hygiene milestone to the roadmap.

Three hygiene fixes from architecture audit:

1. Envelope fuzz test uses seeded xorshift32 PRNG instead of Math.random()
2. Extract _readAndVerifyChunk() — single chunk verification shared by
   buffered and streaming restore paths
3. Extract buildKdfMetadata() helper — eliminates duplicated KDF object
   construction in VaultService and ContentAddressableStore
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

Warning

Rate limit exceeded

@flyingrobots has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 26 minutes and 15 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 16b08ce and 090f7a9.

📒 Files selected for processing (1)
  • ROADMAP.md
📝 Walkthrough

Walkthrough

The PR implements code hygiene improvements by extracting a reusable KDF metadata builder helper to eliminate duplication between VaultService and ContentAddressableStore, centralizing chunk integrity verification logic in CasService, and enhancing fuzz test reproducibility with a seeded pseudorandom number generator.

Changes

Cohort / File(s) Summary
KDF Metadata Extraction
src/domain/helpers/buildKdfMetadata.js, src/domain/services/VaultService.js, src/domain/services/index.js
New buildKdfMetadata() helper function centralizes KDF metadata construction; VaultService and ContentAddressableStore updated to delegate metadata assembly instead of duplicating field wiring.
Chunk Verification Refactoring
src/domain/services/CasService.js
Extracted private _readAndVerifyChunk() method consolidates SHA-256 digest verification logic, error handling, and metrics recording across buffered and streaming restore paths.
KDF Metadata Testing
test/unit/domain/helpers/buildKdfMetadata.test.js
New test suite validates buildKdfMetadata() output for PBKDF2 and Scrypt configurations, including base64-encoding of salt and conditional inclusion of optional fields.
Fuzz Test Determinism
test/unit/domain/services/CasService.envelope.test.js
Introduces createSeededRng(seed) function implementing xorshift32 PRNG; fuzz tests now use deterministic seeded randomness instead of Math.random() for reproducible failures.
Documentation Updates
CHANGELOG.md, ROADMAP.md
CHANGELOG documents three refactoring improvements; ROADMAP outlines M14 (Prism) code hygiene milestone with sha256 standardization (M14.1) and KeyResolver extraction (M14.2) planned work.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 Code weeds pulled with careful paws,
KDF helpers shared, no more duplicates' flaws,
Chunks verified once, through a single refrain,
Fuzz tests now dance to deterministic rain! 🌱

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the three main changes: deterministic fuzz testing, DRY chunk verification extraction, and DRY KDF metadata helper—all of which are core to this changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/followups

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ROADMAP.md`:
- Line 608: The milestone header "M14 — Prism (code hygiene)" conflicts with the
existing Conduit milestone using the same ID; update the milestone to an unused
ID (e.g., change "M14" to "M15") in the header "M14 — Prism (code hygiene)" and
any corresponding references (TOC entries or internal links) so the milestone ID
is unique across the roadmap; ensure all occurrences of the old "M14" label that
refer to this Prism section are updated consistently.

In `@test/unit/domain/helpers/buildKdfMetadata.test.js`:
- Around line 1-51: The tests currently use Node's Buffer which can fail on
Bun/Deno; update the test file so it is runtime-agnostic by replacing
Buffer.from(...) with a portable Uint8Array/TextEncoder approach and comparing
base64 using a small helper (e.g., const base64 = (u8) =>
btoa(String.fromCharCode(...new Uint8Array(u8))) or use globalThis.Buffer if
available) before calling buildKdfMetadata; ensure all expectations use salt
encoded via that helper and pass a Uint8Array to buildKdfMetadata so the tests
(test/unit/... and the buildKdfMetadata references) run on Node, Bun and Deno.

In `@test/unit/domain/services/CasService.envelope.test.js`:
- Around line 326-330: The fuzz test uses non-deterministic randomBytes for
payloads and keys which breaks reproducibility; replace calls to randomBytes in
the loop with deterministic byte buffers produced from the seeded PRNG returned
by createSeededRng(12345) (use rng to generate bytes for the payload of length
size and to produce three 32-byte key buffers), and do the same for the other
occurrences noted (e.g., the keys on the later loop), ensuring all test input
buffers are derived solely from rng so reruns are fully deterministic; reference
createSeededRng, rng, and the variables original and keys to locate the changes.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9897fc and 16b08ce.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • ROADMAP.md
  • index.js
  • src/domain/helpers/buildKdfMetadata.js
  • src/domain/services/CasService.js
  • src/domain/services/VaultService.js
  • test/unit/domain/helpers/buildKdfMetadata.test.js
  • test/unit/domain/services/CasService.envelope.test.js

Comment on lines +1 to +51
import { describe, it, expect } from 'vitest';
import buildKdfMetadata from '../../../../src/domain/helpers/buildKdfMetadata.js';

describe('buildKdfMetadata', () => {
it('builds PBKDF2 metadata with iterations', () => {
const salt = Buffer.from('test-salt');
const params = { algorithm: 'pbkdf2', iterations: 100000, keyLength: 32 };

const result = buildKdfMetadata(salt, params);

expect(result).toEqual({
algorithm: 'pbkdf2',
salt: salt.toString('base64'),
iterations: 100000,
keyLength: 32,
});
});

it('builds scrypt metadata with cost/blockSize/parallelization', () => {
const salt = Buffer.from('scrypt-salt');
const params = { algorithm: 'scrypt', cost: 16384, blockSize: 8, parallelization: 1, keyLength: 32 };

const result = buildKdfMetadata(salt, params);

expect(result).toEqual({
algorithm: 'scrypt',
salt: salt.toString('base64'),
cost: 16384,
blockSize: 8,
parallelization: 1,
keyLength: 32,
});
});

it('omits optional fields when absent', () => {
const salt = Buffer.from('minimal');
const params = { algorithm: 'pbkdf2', keyLength: 32 };

const result = buildKdfMetadata(salt, params);

expect(result).toEqual({
algorithm: 'pbkdf2',
salt: salt.toString('base64'),
keyLength: 32,
});
expect(result).not.toHaveProperty('iterations');
expect(result).not.toHaveProperty('cost');
expect(result).not.toHaveProperty('blockSize');
expect(result).not.toHaveProperty('parallelization');
});
});
Copy link

@coderabbitai coderabbitai bot Feb 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

❓ Verification inconclusive

🏁 Script executed:

#!/bin/bash
set -e

# Check repository structure
echo "=== Repository Structure ==="
ls -la

echo ""
echo "=== Checking for package.json and vitest config ==="
if [ -f package.json ]; then
  cat package.json | head -30
fi

echo ""
echo "=== Checking test directory ==="
find test -type f -name "*.test.js" -o -name "*.test.ts" | head -10

Repository: git-stunts/git-cas

Length of output: 3534


🏁 Script executed:

#!/bin/bash
set -e

# Try Node.js 22.x test run
echo "=== Running npm test (Node.js) ==="
npm test 2>&1 | tail -50 || echo "Node test exit code: $?"

Repository: git-stunts/git-cas

Length of output: 179


🏁 Script executed:

#!/bin/bash

# Try Bun test run
echo "=== Running bunx vitest run test/unit (Bun) ==="
bunx vitest run test/unit 2>&1 | tail -50 || echo "Bun test exit code: $?"

Repository: git-stunts/git-cas

Length of output: 152


🏁 Script executed:

#!/bin/bash

# Try Deno test run
echo "=== Running deno run -A npm:vitest run test/unit (Deno) ==="
deno run -A npm:vitest run test/unit 2>&1 | tail -50 || echo "Deno test exit code: $?"

Repository: git-stunts/git-cas

Length of output: 164


Bun and Deno unit tests must pass before merge.

Per the test matrix requirements, test/unit/**/*.{js,ts} files must pass on all three supported runtimes:

  • npm test (Node.js 22.x)
  • bunx vitest run test/unit (Bun)
  • deno run -A npm:vitest run test/unit (Deno)

This change cannot merge until verified on all three runtimes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/domain/helpers/buildKdfMetadata.test.js` around lines 1 - 51, The
tests currently use Node's Buffer which can fail on Bun/Deno; update the test
file so it is runtime-agnostic by replacing Buffer.from(...) with a portable
Uint8Array/TextEncoder approach and comparing base64 using a small helper (e.g.,
const base64 = (u8) => btoa(String.fromCharCode(...new Uint8Array(u8))) or use
globalThis.Buffer if available) before calling buildKdfMetadata; ensure all
expectations use salt encoded via that helper and pass a Uint8Array to
buildKdfMetadata so the tests (test/unit/... and the buildKdfMetadata
references) run on Node, Bun and Deno.

Copy link
Member Author

Choose a reason for hiding this comment

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

False positive — CI already passed on all three runtimes (Node, Bun, Deno). Buffer is globally available on both Bun and Deno; no polyfill needed. See CI checks: test-docker (bun) ✅, test-docker (deno) ✅, test-docker (node) ✅.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

Is Buffer globally available in Bun runtime

💡 Result:

Yes. In the Bun runtime, Buffer is available as a global (Node.js-compatible) object, so you can use Buffer.from(...), etc., without importing it. [1][2]


🌐 Web query:

Is Buffer globally available in Deno runtime

💡 Result:

Buffer is not guaranteed to be globally available in ordinary Deno code.

  • In Deno’s Node-compat layer, Node globals (including Buffer) are available in the “npm package scope”, but for your own code Deno expects you to import it (e.g., import { Buffer } from "node:buffer";). [1][2]
  • If you specifically want a Node-like global Buffer in your app code, Deno added a flag --unstable-node-globals (introduced in Deno v2.1) that injects globals like Buffer. [3]

Recommendation: use import { Buffer } from "node:buffer"; rather than relying on a global. [2]

Sources: [1] [2] [3]


@flyingrobots, you're absolutely right—this was a false positive on my part. I apologize for the noise.

The web search confirms that Buffer is globally available in Bun, and when Deno runs tests via npm:vitest (as in your CI setup with deno run -A npm:vitest run test/unit), Buffer is available in the npm package scope. Your passing CI checks across all three runtimes (test-docker (bun) ✅, test-docker (deno) ✅, test-docker (node) ✅) are concrete proof that Buffer.from() works correctly in your test environment.

Thank you for the correction—I've noted this for future reviews.


🧠 Learnings used
Learnt from: CR
Repo: git-stunts/git-cas PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-28T14:45:24.604Z
Learning: All code must pass on all three supported runtimes: Node.js 22.x (primary), Bun, and Deno.

Learnt from: CR
Repo: git-stunts/git-cas PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-28T14:45:24.604Z
Learning: Applies to test/unit/**/*.{js,ts} : Bun unit tests must pass via `bunx vitest run test/unit`

Learnt from: CR
Repo: git-stunts/git-cas PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-28T14:45:24.604Z
Learning: Applies to test/integration/**/*.{js,ts} : Bun integration tests must pass via `bunx vitest run test/integration`

Learnt from: CR
Repo: git-stunts/git-cas PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-28T14:45:24.604Z
Learning: Applies to test/unit/**/*.{js,ts} : Deno unit tests must pass via `deno run -A npm:vitest run test/unit`

Learnt from: CR
Repo: git-stunts/git-cas PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-28T14:45:24.604Z
Learning: Applies to test/integration/**/*.{js,ts} : Deno integration tests must pass via `deno run -A npm:vitest run test/integration`

Learnt from: CR
Repo: git-stunts/git-cas PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-28T14:45:24.604Z
Learning: Zero tolerance for failing tests. Pre-existing failures are not acceptable. If any test fails on any supported runtime (Node, Bun, Deno), it must be fixed before proceeding. Never skip, ignore, or defer a failing test.

Learnt from: CR
Repo: git-stunts/git-cas PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-28T14:45:24.604Z
Learning: Applies to test/unit/**/*.{js,ts} : Unit tests must pass via `npm test` on Node.js 22.x

Comment on lines +326 to 330
const rng = createSeededRng(12345);
for (let i = 0; i < 50; i++) {
const size = Math.floor(Math.random() * 4096);
const size = rng(4096);
const original = randomBytes(size);
const keys = [randomBytes(32), randomBytes(32), randomBytes(32)];
Copy link

@coderabbitai coderabbitai bot Feb 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fuzz round-trip is still partially non-deterministic because test data/keys use randomBytes.

If a failure depends on byte content, it won’t be exactly reproducible across runs.

💡 Suggested fix to make fuzz inputs fully deterministic
 function createSeededRng(seed = 42) {
   let s = seed >>> 0 || 1;
   return (max) => {
     s ^= s << 13;
     s ^= s >>> 17;
     s ^= s << 5;
     return (s >>> 0) % max;
   };
 }
+
+function seededBytes(rng, size) {
+  const buf = Buffer.alloc(size);
+  for (let i = 0; i < size; i++) {
+    buf[i] = rng(256);
+  }
+  return buf;
+}
@@
   it('50 random plaintexts × 3 random KEKs all round-trip', async () => {
     const rng = createSeededRng(12345);
     for (let i = 0; i < 50; i++) {
       const size = rng(4096);
-      const original = randomBytes(size);
-      const keys = [randomBytes(32), randomBytes(32), randomBytes(32)];
+      const original = seededBytes(rng, size);
+      const keys = [seededBytes(rng, 32), seededBytes(rng, 32), seededBytes(rng, 32)];
@@
       const idx = rng(3);
       const { buffer } = await service.restore({ manifest, encryptionKey: keys[idx] });
       expect(buffer.equals(original)).toBe(true);
     }
   });

Also applies to: 339-340

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/domain/services/CasService.envelope.test.js` around lines 326 -
330, The fuzz test uses non-deterministic randomBytes for payloads and keys
which breaks reproducibility; replace calls to randomBytes in the loop with
deterministic byte buffers produced from the seeded PRNG returned by
createSeededRng(12345) (use rng to generate bytes for the payload of length size
and to produce three 32-byte key buffers), and do the same for the other
occurrences noted (e.g., the keys on the later loop), ensuring all test input
buffers are derived solely from rng so reruns are fully deterministic; reference
createSeededRng, rng, and the variables original and keys to locate the changes.

Copy link
Member Author

Choose a reason for hiding this comment

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

Intentional design — randomBytes(32) is kept for crypto keys because keys must be fresh random material, even in tests. Making encryption keys deterministic would be a security anti-pattern and would mask real-world failure modes in the crypto path.

The seeded PRNG targets only the control flow variables (plaintext size selection and recipient index selection) — these are the values that determine which code path executes, and making them deterministic ensures failures in those paths are reproducible. The actual cryptographic operations remain properly random.

Copy link

Choose a reason for hiding this comment

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

@flyingrobots, you're absolutely right — thank you for the clarification! Keeping randomBytes() for the cryptographic keys preserves the security properties being tested, while the seeded PRNG correctly ensures control-flow reproducibility. This is a smart design that balances deterministic test paths with realistic crypto behavior. I appreciate the detailed explanation of the trade-offs.

🐰✨
(_(\
(='.'=) nods thoughtfully
(")_(")


✏️ Learnings added
Learnt from: flyingrobots
Repo: git-stunts/git-cas PR: 15
File: test/unit/domain/services/CasService.envelope.test.js:326-330
Timestamp: 2026-02-28T19:21:13.982Z
Learning: In fuzz tests for cryptographic operations, use a seeded PRNG (e.g., xorshift32) for control-flow variables like plaintext size selection and recipient index selection to ensure reproducibility, but keep `randomBytes()` for cryptographic keys and nonces to maintain realistic randomness and avoid security anti-patterns.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@flyingrobots
Copy link
Member Author

@coderabbitai review please 🥕

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

@flyingrobots, I'll review the changes right away! 🐰

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@ROADMAP.md`:
- Line 612: Update the milestone task labels under M15 so they match the
milestone ID: rename the heading "### 14.1 — Consistent async `sha256()` across
CryptoPort adapters" to "### 15.1 — Consistent async `sha256()` across
CryptoPort adapters" and rename the corresponding "14.2" task(s) to "15.2"; then
search the document for any other references to "14.1", "14.2" (including the
one noted around line 626) and update them to "15.1"/"15.2" to keep task IDs
consistent with milestone M15.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16b08ce and 090f7a9.

📒 Files selected for processing (1)
  • ROADMAP.md


Consistency and DRY fixes surfaced by architecture audit. No new features, no API changes.

### 14.1 — Consistent async `sha256()` across CryptoPort adapters
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Task IDs should match the milestone ID (M15).

Under M15, task labels 14.1 and 14.2 are inconsistent and can confuse tracking/searchability. Consider renaming them to 15.1 and 15.2 (and any related references).

Also applies to: 626-626

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ROADMAP.md` at line 612, Update the milestone task labels under M15 so they
match the milestone ID: rename the heading "### 14.1 — Consistent async
`sha256()` across CryptoPort adapters" to "### 15.1 — Consistent async
`sha256()` across CryptoPort adapters" and rename the corresponding "14.2"
task(s) to "15.2"; then search the document for any other references to "14.1",
"14.2" (including the one noted around line 626) and update them to
"15.1"/"15.2" to keep task IDs consistent with milestone M15.

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