Skip to content

[rosary-54ad76] feat(vault): wire kek-source + per-caller rate bucket into Worker#22

Merged
jamestexas merged 3 commits into
mainfrom
feat/vault-wire-kek-source-rate-bucket
May 18, 2026
Merged

[rosary-54ad76] feat(vault): wire kek-source + per-caller rate bucket into Worker#22
jamestexas merged 3 commits into
mainfrom
feat/vault-wire-kek-source-rate-bucket

Conversation

@jamestexas
Copy link
Copy Markdown
Contributor

Summary

Phase A.1.6 of the vault re-incorporation. Brings the two cloister-hardened modules (introduced in #19) into actual use — they were sitting in the tree unused, which the #19 reviewer flagged.

  • VAULT_KEK_SOURCE drives KEK resolution via the URL dispatcher (env://, file://, keychain://, http(s)://). The DO's #getKEK delegates to buildKekSource(spec, env).resolve() instead of reading env.VAULT_KEK_SECRET directly.
  • Rate bucket gates every authenticated request with HTTP 429 + Retry-After when the caller is over budget. State lives in DO memory (Map<sub, BucketState>), charged via a new consumeBudget(sub, costClass) RPC method using the pure refillBucket / tryConsume helpers from rate-bucket.ts. Cost class derived from method + path: PUT=write, DELETE/admin endpoints=read, everything else=proxy.
  • Legacy VAULT_KEK_SECRET path preserved with a one-time console.warn deprecation on first derive — keeps existing deployments working through the rollout.

Identity resolution moved out of the handler-passed callback so the worker can charge the bucket BEFORE delegating. Handler API unchanged (still takes resolveIdentity), so no existing test signatures break.

Test plan

  • All existing vault tests pass: vault-adversarial, vault-security, encryption, vault, worker, kek-source, rate-bucket (134 tests, untouched)
  • New worker-do.test.ts covers:
    • env://X resolves to env.X (full encrypt path)
    • file:// resolves via the KEK_DISK service binding
    • Missing VAULT_KEK_SOURCE → legacy VAULT_KEK_SECRET with exactly one deprecation warning per DO lifetime
    • Missing both → throws no KEK source configured
    • consumeBudget saturates after capacity / cost reqs and returns retryAfterSec >= 1
    • Caller A draining its bucket does not block caller B
    • Cost classes scale: read < write < proxy
  • npx tsc --noEmit clean via worker/tsconfig.json (includes vault/src/**)
  • Total vault test suite: 141 tests pass (134 existing + 7 new)

Cross-references

Out of scope

  • packages/schema-bridge/ (touched by a different bead)
  • pnpm-workspace.yaml, CI config, lockfiles (rosary-546e83 owns those)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR wires previously-introduced vault hardening modules into the actual Worker + Durable Object runtime: KEK resolution is now driven by VAULT_KEK_SOURCE via the kek-source dispatcher, and every authenticated request is gated by a per-caller token bucket enforced inside the CredentialVault DO.

Changes:

  • Add VAULT_KEK_SOURCE-driven KEK resolution in the DO, with a legacy VAULT_KEK_SECRET fallback + one-time deprecation warning.
  • Add a DO RPC consumeBudget(sub, costClass) and call it from the Worker before delegating to the handler, returning 429 + Retry-After when over budget.
  • Add DO-side wiring tests and update docs/config examples for KEK sources and rate budget behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
vault/src/worker.ts Adds KEK source resolution + legacy fallback in the DO, introduces per-caller rate bucket RPC, and charges budget in the Worker before handling requests.
vault/src/__tests__/worker-do.test.ts New tests covering KEK source wiring (env/file/legacy) and per-caller bucket behavior (saturation/isolation/cost scaling).
vault/wrangler.toml.example Documents VAULT_KEK_SOURCE configuration and legacy VAULT_KEK_SECRET deprecation.
vault/README.md Documents KEK source schemes and the per-caller rate budget semantics/limits.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread vault/wrangler.toml.example Outdated
Comment thread vault/README.md Outdated
Comment thread vault/src/worker.ts Outdated
Comment thread vault/src/worker.ts
jamestexas added a commit that referenced this pull request May 18, 2026
…y::collapsible_match)

The outer 'contains("..")' check was strictly redundant — the inner
's == ".."' check (the only one that returned) implies it. Collapse
the pair into a match guard. No behavior change; UDS paths containing
'..' as a substring (e.g. 'foo..bar.sock') are still permitted, only
exact '..' components reject.

CI for the whole repo couldn't pass while this lint was hot under
-D warnings. Unblocks PRs #20/#21/#22 once #23 merges and they rebase.
jamestexas added a commit that referenced this pull request May 18, 2026
* chore(deps): comprehensive workspace dep update sweep

Workspace-wide dependency update sweep across root + action + worker.
Clears all 6 pnpm audit advisories (was 2 high / 4 moderate).

| Workspace | Package                          | From         | To           |
|-----------|----------------------------------|--------------|--------------|
| root      | @vitest/coverage-v8              | ^4.1.2       | ^4.1.6       |
| root      | tsx                              | ^4.21.0      | ^4.22.2      |
| root      | zod                              | ^4.3.6       | ^4.4.3       |
| root      | pnpm.overrides undici            | —            | <6.24.0 → ^6.24.0 |
| root      | pnpm.overrides ws                | —            | <8.20.1 → ^8.20.1 |
| action    | @types/node                      | ^25.5.0      | ^25.9.0      |
| action    | esbuild                          | ^0.25.0      | ^0.25.12     |
| action    | typescript                       | ^5.8.0       | ^5.9.3       |
| worker    | @cloudflare/workers-types        | ^4.20260329.1| ^4.20260518.1|
| worker    | @cloudflare/vitest-pool-workers  | ^0.13.5      | 0.13.5 (PIN) |
| worker    | @playwright/test                 | ^1.59.1      | ^1.60.0      |
| worker    | @types/node                      | ^25.6.0      | ^25.9.0      |
| worker    | prettier                         | ^3.8.1       | ^3.8.3       |
| worker    | vitest                           | ^4.1.2       | ^4.1.6       |
| worker    | wrangler                         | ^4.78.0      | ^4.92.0      |
| worker    | zod                              | ^3.25.0      | ^4.4.3       |
| worker    | oslo                             | ^1.2.1       | REMOVED      |

Notable:
- wrangler 4.78 → 4.92: aligns with notme.bot PR #2 baseline; Node 22 runtime
- zod 3 → 4 in worker: only one file (gha-oidc.ts) uses zod; uses safeParse +
  .error.message which are stable across v3/v4. Aligns with root manifest
  (was already ^4.3.6 there).
- oslo removed: deprecated meta-package; zero imports in src/. Worker already
  uses the successor @oslojs/crypto + @oslojs/encoding directly.
- @cloudflare/vitest-pool-workers pinned to exact 0.13.5 (no caret): per
  rosary-8ae6ab, 0.13.5 has the CF API 10375 issue; we don't yet know if 0.14+
  fixes it. Pin makes the constraint explicit.
- pnpm.overrides force undici≥6.24.0 and ws≥8.20.1: clears all 5 undici
  advisories (transitive via @actions/http-client v2) and the ws advisory
  (transitive via miniflare). Avoids taking the @actions/* major bumps
  (4.0 is ESM-only — separate refactor).

Deferred (need code change or evidence — separate beads):
- @actions/core 1 → 3 / @actions/http-client 2 → 4: ESM-only migration;
  action is currently bundled via esbuild but the ESM-only constraint is a
  real refactor. Advisories handled via pnpm.overrides instead.
- @peculiar/x509 1 → 2: security-sensitive cert API surface; cert-authority.ts
  + signing-authority.ts need careful review of v2's extension/generator API.
- typescript 5 → 6 (action): major TS bump warrants its own pass across the
  workspace, not bundled in deps sweep.
- esbuild 0.25 → 0.28 (action): 0.x bumps frequently change defaults; aligned
  worker is already at 0.28 — leaving action at 0.25 line for now to avoid
  bundling-flag drift.

Test status:
- worker: 425 passed | 6 todo (28 files) — matches baseline
- task worker:check: typecheck + tests both green
- task schema:check: green
- action: pnpm build green, dist/index.js rebuilt and node --check clean
- pnpm audit: 0 advisories (was 2 high / 4 moderate)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ci: trigger run after Actions re-enabled

* fix(proxy): collapse redundant nested if in UDS path validator (clippy::collapsible_match)

The outer 'contains("..")' check was strictly redundant — the inner
's == ".."' check (the only one that returned) implies it. Collapse
the pair into a match guard. No behavior change; UDS paths containing
'..' as a substring (e.g. 'foo..bar.sock') are still permitted, only
exact '..' components reject.

CI for the whole repo couldn't pass while this lint was hot under
-D warnings. Unblocks PRs #20/#21/#22 once #23 merges and they rebase.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
jamestexas and others added 3 commits May 18, 2026 14:27
…ler rate bucket into Worker

Brings the two cloister-hardened modules (introduced in #19) into actual use:
- VAULT_KEK_SOURCE env var drives KEK resolution via the URL dispatcher
  (env://, file://, keychain://, http(s)://). The DO #getKEK delegates
  to buildKekSource() instead of consuming env.VAULT_KEK_SECRET directly.
- RateBucket gates handleRequest with 429 + Retry-After on over-budget
  callers. Bucket state lives in DO memory (Map<sub, BucketState>),
  charged via a new consumeBudget() RPC method using the pure refill /
  tryConsume helpers from rate-bucket.ts. Cost class derived from method
  + path: PUT=write, DELETE/admin=read, everything else=proxy.
- Legacy VAULT_KEK_SECRET path preserved with a one-time console.warn
  deprecation on first derive so existing deployments aren't broken.

Identity resolution is now hoisted out of the handler-passed callback so
the worker can charge the rate bucket BEFORE delegating — handler still
receives the same `resolveIdentity` shape (no API break for tests).

No existing vault-{adversarial,security,encryption,handler}.test.ts
invariants change. New worker-do.test.ts covers env://, file://, legacy
fallback (one-shot warn), missing config (throws), the 429 saturation
path, per-caller isolation, and the read<write<proxy cost ordering.

wrangler.toml.example + README updated with the KEK source choice
matrix, kek-helper sidecar pointer (cloister ADR-0019), and the rate
budget shape (capacity 100, refill 10/s, costs 1/3/5).

Closes rosary-54ad76 (Phase A.1.6).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- wrangler.toml.example + README.md: trim KEK schemes to what
  buildKekSource() actually supports today (env, file, keychain, https).
  Helper-binary schemes (secret-tool/op/apple-password/keyring) deferred
  to follow-up bead rosary-ab33cb.
- worker.ts: hoist route/method validation (preValidateRoute) before
  resolveIdentity() so a flood of garbage paths can't force JWT verify
  + DPoP replay RPC work.
- worker.ts: cap buckets Map at 10_000 entries with LRU eviction
  (delete-then-set on access; evict Map.keys().next() on overflow) to
  bound DO memory growth against a stream of unique sub values.
- worker-do.test.ts: new tests cover (a) preValidateRoute accepts known
  routes and rejects garbage with 400, (b) buckets Map evicts LRU when
  flooded with > BUCKET_CAP unique callers while preserving rate-limit
  correctness for active callers.

141 -> 145 tests passing.
@jamestexas jamestexas force-pushed the feat/vault-wire-kek-source-rate-bucket branch from ef8d5f6 to 0c9623e Compare May 18, 2026 20:27
@jamestexas jamestexas merged commit 1ccd3b0 into main May 18, 2026
3 checks passed
@jamestexas jamestexas deleted the feat/vault-wire-kek-source-rate-bucket branch May 18, 2026 20:33
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.

2 participants