Skip to content

Conversation

@flyingrobots
Copy link
Member

@flyingrobots flyingrobots commented Feb 6, 2026

Implements Milestone 3: Launchpad. Adds native support for Bun and Deno, makes crypto operations async, and sets up secure automated publishing to NPM (OIDC) and JSR.

Summary by CodeRabbit

  • New Features

    • Multi-runtime crypto adapter detection (Bun, Deno, Node).
    • JSR distribution and automated release workflow with provenance.
    • Local git hooks installer and pre-push quality gate script.
    • CLI bumped to v1.3.0.
  • Bug Fixes

    • Fixed recursion issue in Bun crypto operations.
  • Breaking Changes

    • Cryptographic operations (encrypt/decrypt/verify) are now asynchronous.
    • Requires Node.js >= 22.0.0.
  • Improvements

    • Switched CI and builds to pnpm; enhanced package metadata and publish config.
  • Tests

    • Unit tests updated to async patterns to match async crypto APIs.

@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Warning

Rate limit exceeded

@flyingrobots has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 28 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.

📝 Walkthrough

Walkthrough

Runtime-conditional crypto adapters and async crypto operations were added; CasService lazy-initialization implemented; Bun and WebCrypto adapters introduced; CI and release workflows switched to pnpm and a new release pipeline; package metadata and version bumped to 1.3.0; tests and CLI adjusted for async flows.

Changes

Cohort / File(s) Summary
Workflows
.github/workflows/ci.yml, .github/workflows/release.yml
CI switched to pnpm (setup + caching), commands changed to pnpm .... New Release workflow triggers on tag v*, runs multi-runtime integration tests (Node/Bun/Deno) via Docker, and publishes to npm and JSR.
Package & Manifests
package.json, jsr.json, CHANGELOG.md
Version bumped to 1.3.0. Added richer package metadata, jsr.json exports mapping, and changelog rework documenting async crypto, new adapters, and release workflow changes.
Core API / Facade
index.js, bin/git-cas.js
ContentAddressableStore now lazily initializes CasService via async #getService/getService. Many public operations (encrypt/decrypt/store/restore/verify...) converted to async and obtain service per call; CLI restore updated to use awaited service.
Service Layer
src/domain/services/CasService.js
Cryptographic internals _sha256, encrypt, decrypt changed to async; digest and chunk processing now await digests; key validation messages broadened to Buffer
Crypto Adapters
src/infrastructure/adapters/BunCryptoAdapter.js, src/infrastructure/adapters/WebCryptoAdapter.js
Added platform-specific adapters implementing sha256, randomBytes, encryptBuffer, decryptBuffer, and createEncryptionStream with AES‑GCM nonce/tag metadata and key validation logic.
Tests
test/unit/domain/services/*.test.js
Unit tests updated to async patterns: awaiting encrypt/decrypt, switching thrown error assertions to rejects.toThrow, and adjusting key-type expectations.
Dev tooling / Hooks / Docker
Dockerfile, scripts/git-hooks/pre-push, scripts/install-hooks.sh
Dockerfile and test invocations updated to pnpm. Added pre-push hook that runs pnpm lint and pnpm test, and an install script to enable project hooks.

Sequence Diagram

sequenceDiagram
    actor Client
    participant CAS as ContentAddressableStore
    participant Detector as getDefaultCryptoAdapter
    participant Service as CasService
    participant Adapter as CryptoAdapter

    Client->>CAS: encrypt(data, key)
    activate CAS
    CAS->>CAS: `#getService`()
    CAS->>Detector: detect runtime
    activate Detector
    Detector-->>CAS: return Adapter (Bun/Web/Node)
    deactivate Detector
    CAS->>Service: initialize with Adapter
    activate Service
    Service-->>CAS: ready
    CAS->>Service: encrypt(buffer, key)
    activate Service
    Service->>Adapter: encryptBuffer(buffer, key)
    activate Adapter
    Adapter->>Adapter: validate key, gen nonce, AES-GCM
    Adapter-->>Service: { buf, meta }
    deactivate Adapter
    Service-->>CAS: encrypted result
    deactivate Service
    CAS-->>Client: encrypted data
    deactivate CAS
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through async streams,

Lazy services, runtime dreams;
Bun, Web, Node—each gets a key,
Nonces, tags, and harmony;
From v1.2 to v1.3 we leap with glee.

🚥 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 PR title accurately describes the main changes: native Bun/Deno adapter support, async crypto operations, and automated OIDC/JSR release workflow implementation for M3 Launchpad.
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 launchpad

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: 2

🤖 Fix all issues with AI agents
In `@package.json`:
- Around line 77-81: Update the "jsr" dependency entry in package.json from
"^0.13.3" to "^0.14.2" (the "jsr" key in the dependencies/devDependencies block)
and then run your package manager to refresh the lockfile (npm install / yarn
install / pnpm install) so the lockfile reflects the new baseline version.

In `@src/infrastructure/adapters/WebCryptoAdapter.js`:
- Around line 76-105: The encrypt async generator and finalize function
currently share mutable state via this._finalTag, causing a race when multiple
createEncryptionStream/store() calls run concurrently; change to use a
closure-scoped variable (e.g., let finalTag = null) declared in the
createEncryptionStream scope, assign finalTag = fullBuffer.slice(-tagLength)
inside encrypt instead of setting this._finalTag, and have finalize() return
this.#buildMeta(nonce, finalTag); also remove or avoid relying on .bind(this)
for encrypt if it was only used to access _finalTag so the functions close over
the local variable instead of the instance property.
🧹 Nitpick comments (7)
.github/workflows/release.yml (2)

29-34: Docker integration tests missing --build flag.

The package.json scripts (test:integration:node, etc.) use docker compose run --build --rm, but this workflow omits --build. On a fresh CI runner this may work if images are built from scratch, but if the runner has a layer cache from a previous run, stale images could be used, leading to flaky test results against old code.

Proposed fix
      # Integration tests in Docker
      - name: Integration tests (Node)
-        run: docker compose run --rm test-node npx vitest run test/integration
+        run: docker compose run --build --rm test-node npx vitest run test/integration
      - name: Integration tests (Bun)
-        run: docker compose run --rm test-bun bunx vitest run test/integration
+        run: docker compose run --build --rm test-bun bunx vitest run test/integration
      - name: Integration tests (Deno)
-        run: docker compose run --rm test-deno deno run -A npm:vitest run test/integration
+        run: docker compose run --build --rm test-deno deno run -A npm:vitest run test/integration

36-44: No version consistency check between the Git tag and package.json.

A tag like v1.4.0 can be pushed while package.json still reads 1.3.0, causing npm/JSR to publish a version that doesn't match the Git tag. Consider adding a step before publishing to assert the tag matches the package version, e.g.:

- name: Verify tag matches package version
  run: |
    TAG_VERSION="${GITHUB_REF_NAME#v}"
    PKG_VERSION=$(node -p "require('./package.json').version")
    if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
      echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $PKG_VERSION"
      exit 1
    fi
test/unit/domain/services/CasService.crypto.test.js (1)

81-94: Double decrypt call in integrity tests — minor inefficiency.

Each integrity-failure test calls service.decrypt(...) twice: once for rejects.toThrow(CasError) and again inside try/catch to assert err.code. This doubles the crypto work and could mask issues if the two calls behave differently.

A single-call pattern is cleaner:

Example consolidation
-    await expect(service.decrypt({ buffer: buf, key: keyB, meta })).rejects.toThrow(CasError);
-    try {
-      await service.decrypt({ buffer: buf, key: keyB, meta });
-    } catch (err) {
-      expect(err.code).toBe('INTEGRITY_ERROR');
-    }
+    await expect(service.decrypt({ buffer: buf, key: keyB, meta })).rejects.toThrow(
+      expect.objectContaining({ code: 'INTEGRITY_ERROR' }),
+    );

This same pattern recurs in sections 2b, 2c, and the fuzz tamper tests — a single fix can be applied throughout.

src/infrastructure/adapters/BunCryptoAdapter.js (2)

13-14: sha256 is synchronous while the port contract and CasService._sha256 now await the result.

This works because await-ing a non-Promise value simply resolves it. However, it means the Bun adapter's sha256 method signature diverges from the async WebCryptoAdapter.sha256. Consider adding async for consistency with the port contract, especially if the port's JSDoc is ever updated to return Promise<string>.


33-39: decryptBuffer skips #validateKey — inconsistent with encryptBuffer.

encryptBuffer (line 22) and createEncryptionStream (line 42) both call #validateKey(key), but decryptBuffer does not. While CasService validates upstream in restore(), the adapter-level inconsistency could bite if decryptBuffer is ever called directly.

Proposed fix
  decryptBuffer(buffer, key, meta) {
+   this.#validateKey(key);
    const nonce = Buffer.from(meta.nonce, 'base64');
src/infrastructure/adapters/WebCryptoAdapter.js (1)

117-125: #validateKey only checks length, missing type check.

Unlike BunCryptoAdapter.#validateKey and CasService._validateKey, this doesn't reject non-Buffer/Uint8Array inputs (e.g., a 32-char string). The upstream CasService guards this, but for adapter-level consistency and defense-in-depth:

Proposed fix
  `#validateKey`(key) {
+   if (!ArrayBuffer.isView(key) && !(key instanceof ArrayBuffer)) {
+     throw new CasError(
+       'Encryption key must be a Buffer or Uint8Array',
+       'INVALID_KEY_TYPE',
+     );
+   }
    if (key.length !== 32) {
index.js (1)

67-82: Lazy init race: concurrent callers can create duplicate CasService instances.

If two methods (e.g., store and verifyIntegrity) are called concurrently before the service is initialized, both will pass the !this.service check and each will await getDefaultCryptoAdapter() independently, creating two separate CasService instances. The second overwrites the first.

Currently benign since CasService is stateless, but caching the initialization promise is a small, standard safeguard:

Proposed fix
+  `#servicePromise` = null;
+
   async `#getService`() {
-    if (!this.service) {
+    if (!this.#servicePromise) {
+      this.#servicePromise = this.#initService();
+    }
+    return this.#servicePromise;
+  }
+
+  async `#initService`() {
+    if (!this.service) {
       const persistence = new GitPersistenceAdapter({
         plumbing: this.plumbing,
         policy: this.policyConfig
       });
       const crypto = this.cryptoConfig || await getDefaultCryptoAdapter();
       this.service = new CasService({
         persistence,
         chunkSize: this.chunkSizeConfig,
         codec: this.codecConfig || new JsonCodec(),
         crypto,
       });
-    }
+    }
     return this.service;
   }

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

🤖 Fix all issues with AI agents
In @.github/workflows/release.yml:
- Around line 39-47: The release workflow's two steps ("Publish to npm" running
`pnpm publish --provenance --no-git-checks` and "Publish to JSR" running `pnpm
dlx jsr publish`) are not idempotent: if the npm step succeeds but JSR fails,
re-running the job will fail on npm due to an existing version; either make the
npm step tolerant (add continue-on-error or a pre-check for existing package
version before running `pnpm publish`) or split the steps into independent jobs
(e.g., a job that only runs `pnpm publish` and a separate job for `pnpm dlx jsr
publish`) so each can be retried independently. Ensure the chosen fix preserves
provenance/auth env (NODE_AUTH_TOKEN) and that any version-existence check
reliably detects a previously published version before attempting `pnpm
publish`.

In `@src/infrastructure/adapters/BunCryptoAdapter.js`:
- Around line 17-19: The randomBytes method currently returns a Uint8Array from
globalThis.crypto.getRandomValues but the CryptoPort contract expects a Node
Buffer; change randomBytes to convert the Uint8Array into a Buffer before
returning (e.g., call Buffer.from(...) on the result of getRandomValues) so
callers of randomBytes (function randomBytes) receive a Buffer-compatible object
while still using globalThis.crypto.getRandomValues for entropy.
- Around line 33-39: The decryptBuffer method is missing the key validation
call; add a call to the class's private validation helper (`#validateKey`(key)) at
the start of decryptBuffer (before creating the decipher or using key) so
invalid keys produce the same CasError (INVALID_KEY_TYPE / INVALID_KEY_LENGTH)
as encryptBuffer and createEncryptionStream, then proceed to createDecipheriv,
setAuthTag, and return the decrypted Buffer as before.
🧹 Nitpick comments (5)
scripts/install-hooks.sh (2)

15-15: Use the already-computed absolute path for core.hooksPath.

Line 9 computes HOOKS_DIR as an absolute path, but line 15 hardcodes a relative "scripts/git-hooks". Relative core.hooksPath values are resolved from the working tree root, which works for normal git operations, but using the absolute path is more robust and avoids the inconsistency.

Proposed fix
-git config core.hooksPath "scripts/git-hooks"
+git config core.hooksPath "${HOOKS_DIR}"

11-12: Consider making all hooks in the directory executable.

Currently only pre-push is explicitly chmod +x'd. If additional hooks are introduced later, this line would need updating each time.

Future-proof suggestion
-# Make hooks executable
-chmod +x "${HOOKS_DIR}/pre-push"
+# Make all hooks executable
+chmod +x "${HOOKS_DIR}"/*
src/infrastructure/adapters/BunCryptoAdapter.js (1)

41-65: finalize() will throw if called before the encrypt generator is fully consumed.

cipher.getAuthTag() (Line 60) is only valid after cipher.final() (Line 53) has been called inside the generator. If a caller invokes finalize() before fully draining the async generator, it will get an opaque Node error. Consider adding a guard to produce a clear error message.

Proposed defensive guard
   createEncryptionStream(key) {
     this.#validateKey(key);
     const nonce = this.randomBytes(12);
     const cipher = createCipheriv('aes-256-gcm', key, nonce);
+    let finalized = false;
 
     const encrypt = async function* (source) {
       for await (const chunk of source) {
         const encrypted = cipher.update(chunk);
         if (encrypted.length > 0) {
           yield encrypted;
         }
       }
       const final = cipher.final();
       if (final.length > 0) {
         yield final;
       }
+      finalized = true;
     };
 
-    const finalize = () => {
+    const finalize = () => {
+      if (!finalized) {
+        throw new CasError(
+          'Cannot finalize before the encrypt stream is fully consumed',
+          'STREAM_NOT_CONSUMED',
+        );
+      }
       const tag = cipher.getAuthTag();
       return this.#buildMeta(nonce, tag);
     };
Dockerfile (1)

4-7: Consider corepack enable instead of global pnpm install.

Node 22 ships with Corepack, which can manage pnpm without a separate global install. This is a lighter approach and keeps the pnpm version in sync with package.json's "packageManager" field (if set).

♻️ Optional alternative
-RUN npm install -g pnpm@10
+RUN corepack enable && corepack prepare pnpm@10 --activate
.github/workflows/release.yml (1)

8-13: Consider adding a concurrency group to prevent parallel release jobs.

If two version tags are pushed in quick succession, two release jobs could run simultaneously, potentially causing race conditions during publishing.

♻️ Suggested addition (after line 7)
+concurrency:
+  group: release
+  cancel-in-progress: false
+
 jobs:
   release:

@flyingrobots flyingrobots merged commit 80bb5c2 into main Feb 6, 2026
6 checks passed
@flyingrobots flyingrobots deleted the launchpad branch February 6, 2026 12:26
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