Skip to content

Commit 3a3cd3e

Browse files
authored
Merge pull request #36 from git-stunts/cycle/vault-service-decomposition
Resolved PR feedback and release-gate hardening for v6.0.0. Final head 7deb215; checks green; CodeRabbit completed; unresolved review threads: 0.
2 parents cff6be6 + 7deb215 commit 3a3cd3e

145 files changed

Lines changed: 6160 additions & 1062 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ARCHITECTURE.md

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,13 @@ The facade is orchestration glue. It is not the storage engine itself.
130130
compression, integrity verification, recipient mutation, and store/restore
131131
strategy execution to dedicated domain classes.
132132

133-
- **`VaultService`** — manages the GC-safe vault ref (`refs/cas/vault`). Owns
134-
vault initialization, add/update/list/resolve/remove, privacy mode,
135-
history-oriented state reads, and compare-and-swap ref updates with retry on
136-
conflict. It delegates slug validation and plain tree-entry encoding to the
137-
`Slug` value object.
133+
- **`VaultService`** — orchestrates GC-safe vault use cases while keeping the
134+
public vault API stable. It owns initialization, add/update/list/resolve/remove,
135+
and history-oriented state reads, then delegates vault-head persistence to
136+
`VaultPersistence`, parse-stable state memoization to `VaultStateCache`, boundary
137+
formats to `VaultMetadataCodec` and `VaultTreeCodec`, privacy indexing to
138+
`VaultPrivacyIndex`, vault-key verification to `VaultKeyVerifier`, retry timing
139+
to `VaultMutationRetryPolicy`, and slug validation to `Slug`.
138140

139141
- **`KeyResolver`** — resolves key sources: passphrase-derived keys via KDF,
140142
envelope recipient DEK wrapping and unwrapping. `CasService` delegates all key
@@ -354,20 +356,35 @@ still remains authoritative for repeated-chunk order and multiplicity.
354356
### Vault
355357

356358
The vault is a GC-safe slug index rooted at `refs/cas/vault`.
359+
For maintainer-level detail on the collaborators, cache rules, and verifier
360+
flow, see [docs/VAULT_INTERNALS.md](./docs/VAULT_INTERNALS.md).
357361

358362
It is implemented as a commit chain. Each vault commit points to a tree
359363
containing:
360364

361365
- one tree entry per stored slug, mapped to that asset's tree OID
362366
- `.vault.json` metadata for vault configuration
363367

364-
`VaultService` owns:
368+
`VaultService` orchestrates:
365369

366370
- vault initialization
367371
- add, update, list, resolve, remove, and history-oriented state reads
368-
- compare-and-swap ref updates with retry on conflict
369-
- vault metadata validation
370-
- privacy mode
372+
- retrying optimistic vault mutations after compare-and-swap conflicts
373+
374+
The durable vault boundary is split into cohesive collaborators:
375+
376+
- `VaultPersistence` owns the Git substrate: vault-head resolution, tree/blob
377+
reads, commit creation, and compare-and-swap updates to `refs/cas/vault`. It is
378+
stateless and does not cache OIDs.
379+
- `VaultStateCache` owns tree-OID keyed snapshots, parsed entry memoization,
380+
defensive `VaultState` copies, privacy entry maps by key identity, and
381+
verified-key memoization.
382+
- `VaultMetadataCodec` and `VaultTreeCodec` are pure boundary codecs. They encode
383+
and decode `.vault.json`, plain slug tree names, privacy tree names, and mktree
384+
record lines without performing I/O.
385+
- `VaultPrivacyIndex`, `VaultKeyVerifier`, and `VaultMutationRetryPolicy` own the
386+
HMAC privacy index, constant-time vault-key verifier checks, and exponential
387+
backoff with jitter.
371388

372389
Vault slugs are validated and normalized with `Slug`. Plain vault trees encode
373390
slug names through `Slug.toTreePath()`; privacy-enabled vaults keep HMAC tree

CHANGELOG.md

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Breaking Changes
1111

12-
- **JSR support removed** — The JSR registry publication workflow has been removed. `npm run release:verify -- --skip-jsr` now supports skipping JSR dry-runs. Consumers of the `@git-stunts/git-cas` JSR package should migrate to the npm package.
12+
- **JSR publication deferred for v6.0.0** — The npm package and GitHub Release
13+
are the release targets for v6.0.0. JSR metadata and the `jsr-publish`
14+
verification step remain in the repository, while
15+
`npm run release:verify -- --skip-jsr` records the skipped dry-run during the
16+
upstream JSR/Deno toolchain blocker. Consumers of the
17+
`@git-stunts/git-cas` JSR package should migrate to npm for v6.0.0 or stay on
18+
the last JSR-published version.
1319
- **Encryption scheme identifiers simplified**`whole-v1`/`whole-v2` collapsed to `whole`, `framed-v1`/`framed-v2` collapsed to `framed`, `convergent-v1` collapsed to `convergent`. Legacy v1/v2 scheme strings in stored manifests now throw `LEGACY_SCHEME` at `readManifest()` time with migration guidance. The `scheme` field in `ManifestSchema` is now required for all encryption metadata (previously optional for backward-compatible schemeless whole manifests).
1420
- **AAD is always on**`whole` and `framed` encryption always bind slug-based AAD into the GCM tag. The v1 no-AAD path is removed.
1521
- **Core byte contract is now `Uint8Array`** — public and port byte surfaces now accept and return `Uint8Array` rather than Node-specific `Buffer` types. Node callers can continue passing `Buffer` values because `Buffer` extends `Uint8Array`, but restored data, chunkers, codecs, and Web Crypto adapter outputs should be treated as `Uint8Array`.
@@ -28,6 +34,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2834
- **Store/restore pipeline state-machine docs** — added
2935
`docs/STORE_RESTORE_PIPELINE.md` as the maintainer map for store, restore,
3036
tree publication, and vault boundaries.
37+
- **Vault internals maintainer docs** — added
38+
`docs/VAULT_INTERNALS.md` to document the vault collaborator model, cache
39+
rules, boundary codecs, privacy index, key verifier, and retry policy.
40+
- **Public `CasError` export**`CasError` is now re-exported from the package
41+
root for callers that need typed error handling without deep imports.
3142
- **`CasService.readManifestRaw()`** — reads a manifest from a Git tree OID and returns the raw decoded object without Manifest construction or scheme assertion. Migration entry point for inspecting legacy manifests.
3243
- **`CasService` `legacyMode` constructor option** — when `true`, `readManifest()` maps legacy scheme identifiers (v1/v2) to their current names instead of throwing `LEGACY_SCHEME`. Legacy v1 manifests (no AAD) are correctly decrypted without AAD during restore.
3344
- **`mapToCurrentScheme()` and `isLegacyNoAad()` in `schemes.js`** — public helpers for mapping legacy scheme strings to current names and detecting v1 no-AAD schemes.
@@ -73,6 +84,151 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7384
record parsing, and store/restore strategy execution now live in dedicated
7485
domain services and strategy entities with direct unit coverage. Public
7586
`CasService` store/restore/manifest/recipient APIs are unchanged.
87+
- **VaultService decomposed into cohesive collaborators**`VaultService.js`
88+
now orchestrates public vault use cases while `VaultPersistence` owns
89+
`refs/cas/vault` persistence, `VaultStateCache` owns tree-OID keyed state
90+
memoization, `VaultMetadataCodec` and `VaultTreeCodec` own pure boundary
91+
encoding, and dedicated privacy, verifier, and retry-policy collaborators own
92+
HMAC index handling, constant-time key verification, and CAS retry timing.
93+
Public vault APIs and the on-disk vault tree format are unchanged.
94+
- **Privacy vault passphrase rotation preserved** — vault passphrase rotation now
95+
reads metadata before full state so privacy-enabled vaults can derive the old
96+
key, decrypt `.privacy-index`, and rebuild the index under the replacement key.
97+
- **Structured KDF algorithm errors** — unsupported stored or requested KDF
98+
algorithms now fail with `KDF_POLICY_VIOLATION`, and vault metadata decoding
99+
normalizes those policy failures to `VAULT_METADATA_INVALID` instead of
100+
leaking raw `Error` instances.
101+
- **Vault ref creation is create-only** — first vault writes now pass Git's
102+
all-zero expected OID when `expectedOldOid` is `null`, preserving CAS
103+
semantics during concurrent vault initialization.
104+
- **Metadata blob limits reach the default Git adapter**`maxBlobSize`
105+
constructor options now configure `GitPersistenceAdapter.readBlob()` when no
106+
per-call limit is supplied.
107+
- **Git blob per-call limits are validated**`GitPersistenceAdapter.readBlob()`
108+
now rejects invalid caller-provided `maxBytes` limits with `INVALID_OPTIONS`
109+
before opening a Git blob stream.
110+
- **API `maxBlobSize` wording**`docs/API.md` now documents the constructor
111+
option as the metadata blob read limit, matching the runtime service contract.
112+
- **Manifest diff JSDoc boundary**`ManifestDiff.js` now declares its
113+
`Manifest` typedef locally so generated docs and declaration checks can
114+
resolve the pure diff helper parameters.
115+
- **Vault metadata API docs**`docs/API.md` now includes the optional
116+
`privacy` shape in the `VaultMetadata` example alongside the privacy error
117+
codes.
118+
- **Vault keyed caches snapshot key bytes** — privacy-entry and verifier caches
119+
now reject stale hits when a reused `Uint8Array` key object has been mutated.
120+
- **Vault state caches return defensive entry maps**`VaultStateCache` now
121+
copies cached plain and privacy entry maps before returning them, so caller
122+
mutations cannot poison subsequent reads from the same tree snapshot.
123+
- **Vault privacy cache deduplicates in-flight work** — concurrent privacy
124+
reads for the same cached tree and key object now share one `.privacy-index`
125+
resolution instead of decrypting the same index multiple times.
126+
- **Vault tree cache is bounded**`VaultStateCache` now uses a validated
127+
LRU capacity instead of retaining every immutable tree snapshot for the
128+
lifetime of the service.
129+
- **Vault verifier checks reuse cached proofs** — keyed list, resolve, and
130+
mutation paths now reuse the verifier memo stored by `readState()` for the
131+
same immutable vault tree instead of decrypting the verifier repeatedly.
132+
- **Vault verifier cache regression coverage** — mutation memoization tests now
133+
exercise the intended cross-operation path by calling
134+
`readState({ encryptionKey })` before the keyed vault write.
135+
- **Review-feedback test style guards** — privacy error assertions now use
136+
`ErrorCodes` constants, and ManifestDiff declaration checks use regex matching
137+
so benign JSDoc formatting does not break release tests.
138+
- **Stdout-only missing vault refs** — Git ref resolution now treats
139+
`rev-parse refs/cas/vault` failures that only echo the unresolved ref on
140+
stdout as `GIT_REF_NOT_FOUND`, preventing empty-vault initialization flakes
141+
from surfacing as `VAULT_HEAD_INVALID`.
142+
- **Vault metadata enforces the AES-GCM cipher boundary**`.vault.json`
143+
metadata now rejects unsupported `encryption.cipher` values with
144+
`VAULT_METADATA_INVALID`; the v6 vault metadata format remains AES-256-GCM.
145+
- **Vault metadata rejects malformed encryption placeholders**`.vault.json`
146+
payloads with present but falsy `encryption` values now fail with
147+
`VAULT_METADATA_INVALID` instead of being treated as plaintext vaults.
148+
- **Doctor rejects vault heads without metadata**`git cas doctor` now fails
149+
with `VAULT_METADATA_INVALID` when `refs/cas/vault` exists but `.vault.json`
150+
is missing or invalid.
151+
- **Unreadable vault heads stay visible** — vault head resolution now returns an
152+
empty state only when the vault ref is absent; unreadable refs or commits that
153+
cannot resolve to a tree fail with `VAULT_HEAD_INVALID`.
154+
- **Vault ref update failures stay non-retryable unless they are CAS conflicts**
155+
`VaultPersistence` now emits `VAULT_REF_UPDATE_FAILED` for generic
156+
update-ref failures and reserves `VAULT_CONFLICT` for structured
157+
expected-vs-actual OID mismatches.
158+
- **Plumbing missing-ref errors stay non-fatal** — vault head resolution now
159+
recognizes `@git-stunts/plumbing` missing-ref stderr details as an absent
160+
vault while still surfacing unrelated ref failures. Object database failures
161+
and corrupt head stderr are reported as `VAULT_HEAD_INVALID`.
162+
- **Git ref missing errors are structured at the adapter boundary**
163+
`GitRefAdapter.resolveRef()` now normalizes known Git missing-ref stderr to
164+
`GIT_REF_NOT_FOUND`, leaving VaultPersistence's text fallback only for
165+
third-party ref ports.
166+
- **Vault missing-ref fallback documented**`VaultPersistence` now documents
167+
its third-party-port missing-ref stderr fallback as C/English-locale
168+
best-effort behavior; structured `GIT_REF_NOT_FOUND` remains the primary path.
169+
- **Vault metadata snapshot docs**`VaultPersistence.readMetadataSnapshot()`
170+
now explicitly documents that iterator metadata reads avoid full-tree
171+
materialization and therefore return no cache snapshot.
172+
- **VaultService DI guard** — the constructor now rejects mixed
173+
`vaultPersistence` and legacy `persistence`/`ref` injection, and reports a
174+
focused dependency error when the legacy pair is incomplete.
175+
- **Doctor can inspect privacy vaults** — human and agent `doctor` commands now
176+
accept raw vault keys, vault passphrase sources, and OS-keychain targets so
177+
privacy-enabled vaults can be diagnosed without falling back to a missing-key
178+
failure. Agent diagnostics now ignore passphrase input with a warning when the
179+
vault is plaintext, and the TUI operations doctor forwards the already-unlocked
180+
vault key.
181+
- **Privacy index mismatches fail closed** — privacy-mode `readState()`,
182+
`listVault()`, and doctor scans now fail with `VAULT_PRIVACY_INDEX_INVALID`
183+
when `.privacy-index` does not cover every raw HMAC tree entry, avoiding
184+
partial listings that could hide vault corruption.
185+
- **Privacy index metadata fails closed** — privacy-enabled vaults missing
186+
`privacy.indexMeta` now fail with structured `VAULT_PRIVACY_INDEX_INVALID`
187+
metadata before decrypting or resolving privacy-mode entries.
188+
- **Doctor reports byte-level dedupe** — vault stats and doctor output now
189+
include total chunk bytes, unique chunk bytes, duplicate chunk bytes, and a
190+
byte-level dedupe ratio alongside chunk-reference counts.
191+
- **TUI doctor dashboard shows byte economics** — the health dashboard now
192+
renders chunk bytes, unique chunk bytes, duplicate chunk bytes, and the
193+
byte-level dedupe ratio instead of only reference counts.
194+
- **Recipient rotation scans every candidate** — unlabeled `rotateKey()` now
195+
attempts every recipient unwrap before selecting the first match, reducing
196+
recipient-position timing leakage while preserving existing rotation results.
197+
- **Behavior-focused vault tests** — removed the source-layout-only
198+
`VaultService` structure test and added a test-style guard against
199+
`.structure.test.js` files.
200+
- **Current vault tree-path terminology** — renamed the stale
201+
`encodeSlug.test.js` coverage to `VaultTreePath.test.js` and updated comments
202+
to describe the `Slug` tree-path boundary.
203+
- **Facade restore guidance links to versioned docs** — missing
204+
`restoreFile({ baseDirectory })` errors now serialize a v6.0.0 API docs URL
205+
and use the centralized `INVALID_OPTIONS` error code.
206+
- **Restore path symlink boundary**`restoreFile()` now canonicalizes
207+
existing path components before stream or bounded-file publication, blocking
208+
symlinked output directories that resolve outside `baseDirectory`.
209+
- **CLI restore output validation** — restore target resolution now rejects
210+
empty `--out` values with `INVALID_OPTIONS` instead of resolving them to the
211+
current directory.
212+
- **Vault retry policies validate injected hooks**`VaultMutationRetryPolicy`
213+
now rejects non-function `random`/`sleep` dependencies at construction and
214+
freezes configured policy instances.
215+
- **Walkthrough documents per-operation Merkle thresholds** — Merkle guidance
216+
now shows `storeFile({ merkleThreshold })` as the primary override and keeps
217+
constructor-level thresholds framed as defaults.
218+
- **VaultService module header normalized** — the fileoverview block now
219+
appears before imports, and the service header imports errors through the
220+
internal errors barrel.
221+
- **Per-operation Merkle threshold**`store()` and `storeFile()` now accept a
222+
`merkleThreshold` option that carries through to the corresponding
223+
`createTree()` publication unless an explicit `createTree()` threshold is
224+
supplied.
225+
- **Restore guidance surfaced in errors and docs** — missing `restoreFile()`
226+
`baseDirectory` errors now explain the trusted-local `process.cwd()` option,
227+
structured CLI/agent errors can include documentation URLs, and the v6 docs
228+
call out the mandatory restore boundary.
229+
- **Metadata blob limit constantized**`GitPersistenceAdapter` now uses a
230+
named `DEFAULT_MAX_BLOB_SIZE` constant for the default 10 MiB metadata-read
231+
cap and reports the effective limit in `RESTORE_TOO_LARGE` errors.
76232
- **OS-keychain passphrase lookup awaits vault v2 secrets** — CLI credential
77233
resolution now awaits the async `@git-stunts/vault` secret lookup before
78234
validating and returning the passphrase.
@@ -123,10 +279,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
123279

124280
### Fixed
125281

282+
- **Agent diagnostic passphrase resolver guard** — encrypted `git cas agent
283+
doctor` requests now fail with a controlled credential error when a structured
284+
passphrase source is supplied without the resolver dependency.
285+
- **Doctor byte dedupe metric** — vault health statistics now compute byte
286+
dedupe from stored chunk bytes instead of logical file size, keeping
287+
compression and deduplication signals separate.
288+
- **Docker version fallback** — CLI version resolution now ignores the
289+
`unknown` build metadata sentinel written when Docker test images have neither
290+
`.git` metadata nor a stamped package SHA, so `git-cas --version` falls back to
291+
plain semver instead of emitting `+unknown`.
292+
- **Docker unit-test stability** — vault passphrase-rotation unit coverage now
293+
uses in-memory persistence and ref ports, keeping domain behavior validation
294+
independent from Docker Git subprocess scheduling.
126295
- **Shared CLI/agent credential resolution** — human CLI and agent protocol
127296
flows now use `bin/credentials.js` for key-file length checks, ambiguous
128297
credential-source rejection, vault passphrase-derived key verification, and
129298
encrypted-restore input classification.
299+
- **CLI restore output authority** — human and agent CLI restore commands now
300+
treat an explicit `--out` path as authority to write in that path's parent
301+
directory, while `restoreFile()` keeps enforcing its library-level
302+
`baseDirectory` boundary. The low-level path check now uses path-relative
303+
containment instead of a string-prefix comparison.
130304
- **Type declaration accuracy**`CasServiceOptions` now marks `chunker` and `compressionAdapter` as required for direct domain-service construction, and `StoreEncryptionOptions` exposes the supported `convergent` opt-in/opt-out flag.
131305
- **Constructor validation consistency** — direct `CasService` construction now
132306
validates all required ports through the unified constructor argument

CONTRIBUTING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,12 @@ Rules:
192192

193193
The version and tag should reflect shipped reality, not hopeful scope.
194194

195+
Before any release-candidate push, tag prep, or PR that changes public release
196+
behavior, run `npm run release:verify`. If the external JSR/Deno toolchain is
197+
the only known blocker for the current release, use
198+
`npm run release:verify -- --skip-jsr` and record that skipped step in the
199+
release notes or PR verification summary.
200+
195201
## Testing Rules
196202

197203
Tests must be deterministic.

0 commit comments

Comments
 (0)