Skip to content

Fix aggregate storage entitlement usage across buckets - #1010

Merged
kody-bot merged 2 commits into
mainfrom
cursor/track-3-storage-entitlement-multibucket-bebe
Jul 29, 2026
Merged

kody-bot merged 2 commits into
mainfrom
cursor/track-3-storage-entitlement-multibucket-bebe

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • count every inventoried StorageRunner bucket when enforcing storage_bytes
  • include the current bucket before asynchronous registration completes, without double-counting it
  • cap concurrent Durable Object estimate reads at 16 while retaining exact aggregate accounting
  • fail closed with an explicit entitlement-verification error if any bucket estimate is unreadable
  • cover aggregate denial, unchanged single-bucket boundary behavior, and unregistered bucket exclusion

Testing

  • npm run test:workers -- --run packages/worker/src/storage-runner.workers.test.ts (8 tests passed)
  • npm run validate (passed before and after AI review fixes)
System recap — extends an existing primitive (medium risk)

Mode: recap · Base: main @ 6620d1b3 · Head: 69ff2f95

Classification: extends — storage entitlement enforcement now aggregates every bucket in the per-user durable storage inventory.

Primitives touched

Primitive Group Impact
durable-storage assistant extends — quota checks aggregate all inventoried bucket estimates

System map

A storage write reads the acting user's bucket inventory, obtains bounded-concurrency estimates from those user-scoped Durable Objects, and combines them with D1 usage before enforcement.

Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).

flowchart LR
	inventory["d1-app-db<br/>Per-user bucket inventory"]:::untouched
	storage["durable-storage<br/>Durable storage buckets"]:::extended
	entitlement["entitlements<br/>Resource enforcement"]:::untouched
	inventory -->|"user_id-scoped storage IDs"| storage
	storage -->|"batched estimated-byte total"| entitlement
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Invariants

  • Every inventory query and Durable Object name remains scoped by userId.
  • Unregistered buckets are excluded unless they are the current bucket being written; the current bucket is deduplicated after registration.
  • Concurrent estimate fan-out is capped at 16 while all inventoried buckets remain counted.
  • Unreadable estimates fail entitlement verification closed rather than being treated as zero usage.

Conductor Report

STATUS: done

What changed: storage-byte entitlement checks now sum D1 bytes plus every inventoried per-user StorageRunner bucket, with the current bucket included and deduplicated during registration races. Durable Object estimate reads run in batches of at most 16 and fail closed with an explicit verification error if unreadable.

Tests run: focused Workers suite passed (8/8). npm run validate passed twice, including after AI review fixes; formatting, lint, typecheck, Node/Workers tests, Playwright E2E, MCP E2E, backup build, primitives, and migrations checks are green.

Blocker: none.

Scope spill: none.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • Bug Fixes
    • Storage entitlement checks now aggregate current storage usage across all inventoried storage buckets a user owns, instead of only the bucket being written.
    • Writes are blocked more reliably when the combined storage footprint would exceed the user’s entitlement.
    • Buckets removed from the ownership inventory are excluded from entitlement calculations, while previously stored data remains readable.
  • Tests
    • Added coverage to verify entitlement aggregation behavior across multiple inventoried buckets and correct exclusion after inventory removal.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Storage write entitlement checks now aggregate D1 usage and durable-object estimates across inventoried user storage buckets. Tests cover over-entitlement rejection, ownership removal, boundary writes, and retained bucket data.

Changes

Storage entitlement aggregation

Layer / File(s) Summary
Aggregate inventoried storage estimates
packages/worker/src/storage-runner.ts
Write authorization combines D1 usage with bounded concurrent estimates from the user’s inventoried storage buckets and the incoming storage id.
Validate bucket inventory entitlement behavior
packages/worker/src/storage-runner.workers.test.ts
Tests verify entitlement rejection across two buckets, exclusion after ownership removal, boundary success, and continued retrieval of existing data.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WriteAuthorization
  participant StorageBucketService
  participant StorageRunnerRPC
  participant D1Storage
  WriteAuthorization->>D1Storage: readUserD1StorageBytes
  WriteAuthorization->>StorageBucketService: listUserStorageBucketIds
  WriteAuthorization->>StorageRunnerRPC: getEstimatedBytes for inventoried storage ids
  StorageRunnerRPC-->>WriteAuthorization: estimated durable-object bytes
  D1Storage-->>WriteAuthorization: current D1 bytes
Loading

Possibly related PRs

  • kentcdodds/kody#965: Adds and wires user_storage_buckets, the ownership source used by this entitlement aggregation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: aggregate storage entitlement enforcement across buckets.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/track-3-storage-entitlement-multibucket-bebe

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.

@kody-bot
kody-bot marked this pull request as ready for review July 29, 2026 12:54
@cursor

cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review

@github-actions

github-actions Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-1010.kody-a99.workers.dev

Worker: kody-pr-1010
D1: kody-pr-1010-db
KV: kody-pr-1010-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/worker/src/storage-runner.workers.test.ts (2)

243-296: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test math is sound but silently depends on estimateA > 0 and rawSize: 0.

targetD1Bytes - initialD1Bytes only lands on targetD1Bytes because the seeded row starts with raw_size = 0, and the denial at Line 292 only holds if estimateA > 0. Both are true today but neither is asserted, so a future change to getEstimatedBytes or the seed helper would make this test pass vacuously. Adding expect(estimateA).toBeGreaterThan(0) (and asserting the post-update D1 total) would make the intent explicit.

♻️ Suggested assertions
 	const estimateA = (await runnerA.getEstimatedBytes()).estimatedBytes
 	const estimateB = (await runnerB.getEstimatedBytes()).estimatedBytes
+	expect(estimateA).toBeGreaterThan(0)
+	expect(estimateB).toBeGreaterThan(0)
 	const initialD1Bytes = await readUserD1StorageBytes({
 		db: env.APP_DB,
 		userId,
 	})
 	const targetD1Bytes = limit - estimateB - 1
 	await env.APP_DB.prepare(
 		`UPDATE email_messages SET raw_size = ? WHERE user_id = ?`,
 	)
 		.bind(targetD1Bytes - initialD1Bytes, userId)
 		.run()
+	await expect(
+		readUserD1StorageBytes({ db: env.APP_DB, userId }),
+	).resolves.toBe(targetD1Bytes)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/storage-runner.workers.test.ts` around lines 243 - 296,
Add explicit assertions in the storage entitlement test after calculating
estimateA and after updating email_messages: assert estimateA is greater than
zero, and read/assert the resulting D1 storage total equals targetD1Bytes. Keep
the existing raw_size: 0 setup and aggregate denial assertions unchanged.

277-291: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer Vitest's rejection matchers over manual .then(null, capture).

await expect(...).rejects.toBeInstanceOf(EntitlementLimitError) plus a captured error would express the same intent without the hand-rolled sentinel and throw new Error(...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/storage-runner.workers.test.ts` around lines 277 - 291,
Replace the manual rejection capture and instanceof check around
assertStorageRunnerWriteWithinEntitlement with Vitest’s
expect(...).rejects.toBeInstanceOf(EntitlementLimitError) matcher. Remove the
aggregateDenied sentinel and custom throw while preserving the existing
entitlement-rejection assertion.
packages/worker/src/storage-runner.ts (1)

597-621: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Per-write DO fan-out now scales with bucket count.

Every storage write triggers one inventory query plus N DO round-trips, serialized across batches of 16. For users who accumulate many exec:* buckets this becomes the dominant latency of the write path. Consider a short-TTL memo of the aggregate (keyed by userId) or persisting per-bucket estimates in user_storage_buckets so the entitlement check reads D1 only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/storage-runner.ts` around lines 597 - 621, The storage
write path currently performs a DO estimate RPC for every registered storage
bucket; reduce this fan-out by caching the aggregate durable-object byte
estimate with a short TTL keyed by userId, or persist per-bucket estimates in
user_storage_buckets and read the aggregate from D1. Update the code surrounding
durableObjectBytes and preserve the entitlement calculation while avoiding
per-write DO round-trips.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/src/storage-runner.ts`:
- Around line 606-616: Update the concurrent estimate reads in the getCurrent
flow around storageRunnerRpc().getEstimatedBytes() so one failing StorageRunner
does not reject the entire Promise.all operation. Use Promise.allSettled and
apply each unreadable bucket’s last known estimate, or, if the intended policy
is fail-closed, catch and rethrow an explicit entitlement-related error;
preserve successful estimates and the existing batching behavior.

---

Nitpick comments:
In `@packages/worker/src/storage-runner.ts`:
- Around line 597-621: The storage write path currently performs a DO estimate
RPC for every registered storage bucket; reduce this fan-out by caching the
aggregate durable-object byte estimate with a short TTL keyed by userId, or
persist per-bucket estimates in user_storage_buckets and read the aggregate from
D1. Update the code surrounding durableObjectBytes and preserve the entitlement
calculation while avoiding per-write DO round-trips.

In `@packages/worker/src/storage-runner.workers.test.ts`:
- Around line 243-296: Add explicit assertions in the storage entitlement test
after calculating estimateA and after updating email_messages: assert estimateA
is greater than zero, and read/assert the resulting D1 storage total equals
targetD1Bytes. Keep the existing raw_size: 0 setup and aggregate denial
assertions unchanged.
- Around line 277-291: Replace the manual rejection capture and instanceof check
around assertStorageRunnerWriteWithinEntitlement with Vitest’s
expect(...).rejects.toBeInstanceOf(EntitlementLimitError) matcher. Remove the
aggregateDenied sentinel and custom throw while preserving the existing
entitlement-rejection assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 558961a3-8237-42d1-a512-d7d38ddb552a

📥 Commits

Reviewing files that changed from the base of the PR and between 6620d1b and ffea311.

📒 Files selected for processing (2)
  • packages/worker/src/storage-runner.ts
  • packages/worker/src/storage-runner.workers.test.ts

Comment thread packages/worker/src/storage-runner.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
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.

3 participants