Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ node_modules/
.DS_Store
.vite/
coverage/
.claude/
lastchat.txt
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- **`CryptoPortBase.sha256()` type** — `index.d.ts` declaration corrected from `string | Promise<string>` to `Promise<string>`, matching the async implementation since v5.2.3.
- **`keyLength` passthrough** — `KeyResolver.#resolveKeyFromPassphrase` and `deriveKekFromKdf` now forward `kdf.keyLength` to `deriveKey()`, fixing a latent bug for vaults configured with non-default key lengths.
- **Deno test compatibility** — `createCryptoAdapter.test.js` no longer crashes on Deno by guarding immutable `globalThis.Deno` restoration with try/catch and skipping Node-only tests on non-Node runtimes.
- **README wording** — "no public API changes" corrected to "no breaking API changes" in the v5.2.3 summary.
- **Barrel re-export description** — README and CHANGELOG now show the correct `export { default as X } from '...'` syntax.
- **Vestigial `lastchat.txt`** removed from `jsr.json` exclude list.

### Changed
- **`keyResolver` is now private** — `CasService.keyResolver` changed to `#keyResolver`, preventing external access to an internal implementation detail.
- **`VaultPassphraseRotator.js` → `rotateVaultPassphrase.js`** — renamed to follow camelCase convention for files that export a function (PascalCase is reserved for classes).
- **`resolveChunker` validation** — `chunkSize` now validated as a finite positive number before constructing `FixedChunker`; invalid values fall through to CasService default.
- **`@fileoverview` JSDoc** added to `FileIOHelper.js`, `createCryptoAdapter.js`, and `resolveChunker.js`.
- **`KeyResolver` design note** — class JSDoc now documents the direct `CryptoPort.deriveKey()` call (bypasses `CasService.deriveKey()`).
- **Long function signature wrapped** — `rotateVaultPassphrase()` export signature broken across multiple lines.
- **Test hardening** — salt assertion in `KeyResolver.resolveForStore`, `keyLength` round-trip test, `resolveChunker` edge-case tests, guarded `rmSync` teardown in `FileIOHelper.test.js`.

## [5.2.3] — Prism refactor (2026-03-03)

### Changed
- **Async `sha256()` across all adapters** — `NodeCryptoAdapter.sha256()` now returns `Promise<string>` (was sync `string`), matching Bun and Web adapters. Fixes Liskov Substitution violation; all callers already `await`. `CryptoPort` JSDoc and `CasService.d.ts` updated to `Promise<string>`.
- **Extract `KeyResolver`** — ~170 lines of key resolution logic (`wrapDek`, `unwrapDek`, `resolveForDecryption`, `resolveForStore`, `resolveRecipients`, `resolveKeyForRecipients`, passphrase derivation, mutual-exclusion validation) extracted from `CasService` into `src/domain/services/KeyResolver.js`. CasService delegates via `this.keyResolver`. No public API changes. 24 new unit tests.
- **Move `createCryptoAdapter`** — runtime crypto detection moved from `index.js` to `src/infrastructure/adapters/createCryptoAdapter.js`; test helper now delegates instead of duplicating.
- **Factor out `resolveChunker`** — chunker factory resolution moved from `index.js` private method to `src/infrastructure/chunkers/resolveChunker.js`.
- **Move file I/O helpers** — `storeFile()` and `restoreFile()` moved from `index.js` to `src/infrastructure/adapters/FileIOHelper.js`; all `node:*` imports removed from facade.
- **Factor out `rotateVaultPassphrase`** — passphrase rotation orchestration (~100 lines with retry/backoff) moved from `index.js` to `src/domain/services/rotateVaultPassphrase.js`; `CasError` and `buildKdfMetadata` imports removed from facade.
- **Private `#config` field** — facade constructor stores options in a single private `#config` field instead of 10 public `this.fooConfig` properties.
- **Barrel re-exports** — 10 re-export-only modules (`NodeCryptoAdapter`, `Manifest`, `Chunk`, ports, observers, chunkers) converted to `export { default as X } from '...'` form, eliminating unnecessary local bindings.
- **Configurable retry** — `rotateVaultPassphrase()` now accepts optional `maxRetries` (default 3) and `retryBaseMs` (default 50) options for tuning optimistic-concurrency backoff.
- **Deterministic fuzz test** — envelope fuzz round-trip test now uses a seeded xorshift32 PRNG instead of `Math.random()`, making failures reproducible across runs.
- **DRY chunk verification** — extracted `_readAndVerifyChunk()` in `CasService`; both the buffered and streaming restore paths now delegate to the same single-chunk verification method.
- **DRY KDF metadata** — extracted `buildKdfMetadata()` helper (`src/domain/helpers/buildKdfMetadata.js`); `VaultService` and `ContentAddressableStore` both call it instead of duplicating the KDF object construction.
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ We use the object database.

<img src="./docs/demo.gif" alt="git-cas demo" />

## What's new in v5.2.3

**Internal refactoring — no breaking API changes.** The facade, CasService, and crypto adapters were restructured for better separation of concerns:

- **Consistent async `sha256()`** — `NodeCryptoAdapter.sha256()` now returns `Promise<string>` like Bun and Web adapters, fixing a Liskov Substitution violation.
- **`KeyResolver` extracted** — ~170 lines of key resolution logic (DEK wrap/unwrap, passphrase derivation, envelope recipients) moved from CasService (1085 → 909 lines) into a dedicated `KeyResolver` service.
- **Facade decomposed** — `createCryptoAdapter`, `resolveChunker`, `FileIOHelper`, `rotateVaultPassphrase`, and `buildKdfMetadata` extracted from the monolithic `index.js` into focused modules.
- **Barrel re-exports** — 10 re-export-only modules converted to `export { default as X } from '...'` form.

See [CHANGELOG.md](./CHANGELOG.md) for the full list of changes.

## What's new in v5.2.1

Bug fix: `rotateVaultPassphrase` now honours `kdfOptions.algorithm` — previously the `--algorithm` flag was silently ignored, always reusing the old KDF algorithm. CLI flag tables in `docs/API.md` are now split per command with `--cwd` documented.
Expand Down
41 changes: 20 additions & 21 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -605,48 +605,47 @@ All tasks completed (13.1–13.6). See [COMPLETED_TASKS.md](./COMPLETED_TASKS.md

---

## M15 — Prism (code hygiene)
## M15 — Prism (code hygiene)

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

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

**Problem:** `NodeCryptoAdapter.sha256()` returns `string` (sync), while `BunCryptoAdapter` and `WebCryptoAdapter` return `Promise<string>`. Callers must defensively `await` every call. This is a Liskov Substitution violation — adapters are not interchangeable without the caller knowing which one it got.

**Fix:** Make `NodeCryptoAdapter.sha256()` return `Promise<string>` (wrap the sync result). All three adapters then have the same async signature. CasService already awaits every call via `_sha256()`, so no call-site changes are needed outside the adapter itself.

**Files:**
- `src/infrastructure/adapters/NodeCryptoAdapter.js` — wrap return in `Promise.resolve()`
- `src/infrastructure/adapters/NodeCryptoAdapter.js` — add `async` keyword
- `src/ports/CryptoPort.js` — update JSDoc to document `Promise<string>` as the contract
- `src/domain/services/CasService.d.ts` — update `CryptoPort.sha256` type signature

**Risk:** None. All callers already `await`. Changing sync→async is backward compatible for awaiting code.

**Tests:** Existing crypto adapter tests already assert on the resolved value. Add an explicit test: `expect(adapter.sha256(buf)).toBeInstanceOf(Promise)`.
**Tests:** Explicit `expect(adapter.sha256(buf)).toBeInstanceOf(Promise)` test added.

### 14.2 — Extract `KeyResolver` from CasService
### 15.2 — Extract `KeyResolver` from CasService

**Problem:** `CasService` is a ~1087-line god object. Key resolution logic (`_resolveDecryptionKey`, `_resolvePassphraseForDecryption`, `_resolveKeyForRecipients`, `_unwrapDek`, `_wrapDek`, `_validateKeySourceExclusive`) is ~70 lines of self-contained logic that has nothing to do with chunking, storage, or manifests. It's a distinct responsibility: "given a manifest and caller-provided credentials, produce the decryption key."
**Problem:** `CasService` is a ~1085-line god object. Key resolution logic (~170 lines) is a distinct responsibility: "given a manifest and caller-provided credentials, produce the decryption key."

**Fix:** Extract a `KeyResolver` class into `src/domain/services/KeyResolver.js`. It receives a `CryptoPort` via constructor injection. CasService delegates to it.
**Fix:** Extracted `KeyResolver` class into `src/domain/services/KeyResolver.js`. Receives `CryptoPort` via constructor injection. CasService delegates via `this.keyResolver`.

**API:**
```js
class KeyResolver {
constructor(crypto) { this.crypto = crypto; }
async resolveForDecryption(manifest, encryptionKey, passphrase) { ... }
async resolveForStore(encryptionKey, passphrase, kdfOptions) { ... }
async resolveRecipients(recipients) { ... }
async wrapDek(dek, kek) { ... }
async unwrapDek(recipientEntry, kek) { ... }
}
```
**Extracted methods:**
- `validateKeySourceExclusive()` (static) — mutual-exclusion guard
- `wrapDek()` / `unwrapDek()` — DEK envelope operations
- `resolveForDecryption()` — manifest + credentials → decryption key
- `resolveForStore()` — key/passphrase → store-ready key + metadata
- `resolveRecipients()` — multi-recipient DEK generation
- `resolveKeyForRecipients()` — envelope unwrap with fallback iteration
- `#resolvePassphraseForDecryption()` — passphrase → key via manifest KDF
- `#resolveKeyFromPassphrase()` — passphrase + KDF params → derived key

**Files:**
- New: `src/domain/services/KeyResolver.js`
- Modified: `src/domain/services/CasService.js` — delegate key resolution
- New: `test/unit/domain/services/KeyResolver.test.js`
- Modified: `src/domain/services/CasService.js` — 1085 → 909 lines
- New: `test/unit/domain/services/KeyResolver.test.js` — 24 tests

**Risk:** Low — internal refactor, no public API change. CasService methods remain unchanged.
**Risk:** None — internal refactor, no public API change. CasService methods remain unchanged.

---

Expand Down
6 changes: 5 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export declare class CdcChunker extends ChunkingPort {

/** Abstract port for cryptographic operations. */
export declare class CryptoPortBase {
sha256(buf: Buffer): string | Promise<string>;
sha256(buf: Buffer): Promise<string>;
randomBytes(n: number): Buffer;
encryptBuffer(
buffer: Buffer,
Expand Down Expand Up @@ -401,6 +401,10 @@ export default class ContentAddressableStore {
oldPassphrase: string;
newPassphrase: string;
kdfOptions?: Omit<DeriveKeyOptions, "passphrase">;
/** Maximum optimistic-concurrency retries on VAULT_CONFLICT. @default 3 */
maxRetries?: number;
/** Base delay in ms for exponential backoff between retries. @default 50 */
retryBaseMs?: number;
}): Promise<{
commitOid: string;
rotatedSlugs: string[];
Expand Down
Loading