Per-user plans and entitlement/quota enforcement - #619
Conversation
…forcement - users.plan column (nullable; NULL = legacy/unlimited) and entitlement_daily_counters table in migration 0048 - packages/worker/src/entitlements/: plan definitions, per-plan limits, EntitlementLimitError (one typed shape + one user-facing message), and assertWithinEntitlement with built-in D1 usage counters - Exemplar enforcement point: createJob (scheduled_jobs resource) - Shared workflow status list extracted for the concurrent-workflows counter - Architecture doc: docs/contributing/architecture/entitlements.md - entitlement_daily_counters added to the account-deletion cascade Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
One enforcement point per billable resource, all throwing the shared EntitlementLimitError via assertWithinEntitlement: - saved packages: package_save create branch and the projection insert in refreshSavedPackageProjection (repo publish path fails open without email) - package services: service_start gates persistent mode and the running service count; already-running restarts are exempt - repo sessions: repo_open_session new-session path (resume exempt) - email sends/day: sendOutboundEmail asserts then increments the daily counter for every user; email_send and email_reply pass the account email - secrets: saveSecret new-entry branch; wired from secret_set, the account secrets UI, and the generated-UI API - workflows: createDynamicCallableWorkflow uses the entitlements path with the WORKFLOW_CONCURRENT_LIMIT env var absorbed as the NULL-plan backstop; workflow status values deduped into workflow-statuses.ts Shared entitlement test schema added for workers suites (email tests provision users.plan and entitlement_daily_counters). Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
📝 WalkthroughWalkthroughThis PR introduces a plan-based entitlements system ( ChangesEntitlements system
Estimated code review effort: 4 (Complex) | ~75 minutes Sequence Diagram(s)sequenceDiagram
participant Handler as MCP Capability Handler
participant Entitlements as assertWithinEntitlement / consumeDailyEntitlement
participant D1 as D1 Database
participant Error as EntitlementLimitError
Handler->>Entitlements: db, userId, email, resource
Entitlements->>D1: SELECT plan FROM users (hash-verified email)
D1-->>Entitlements: plan or NULL
alt plan is NULL/unknown
Entitlements-->>Handler: allow (legacy unlimited)
else plan resolved
Entitlements->>D1: count usage / read daily counter
D1-->>Entitlements: current count
alt current + requested exceeds limit
Entitlements->>Error: build EntitlementLimitError(details)
Error-->>Handler: throw
else within limit
Entitlements->>D1: increment counter (if daily)
Entitlements-->>Handler: allow
end
end
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
…rage) Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/worker/src/jobs/service.ts (1)
924-936: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftAtomic quota enforcement needed
Two concurrentcreateJobcalls can still bypassscheduled_jobs:assertWithinEntitlementreads the current count beforeinsertJobRow, and nothing here ties the check to the insert. If this is a hard quota, make the check + insert atomic or enforce it at the DB layer.🤖 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/jobs/service.ts` around lines 924 - 936, The quota check in createJob is race-prone because assertWithinEntitlement runs before insertJobRow with no atomic tie between them. Update createJob so the scheduled_jobs entitlement check and job insertion happen atomically, either by moving the limit enforcement into the same DB transaction used for insertJobRow or by enforcing the quota directly at the database layer. Use createJob, assertWithinEntitlement, and insertJobRow as the key entry points when making the fix.packages/worker/src/app/handlers/account-secrets.ts (1)
228-341: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winWrap
saveSecretin the OAuth connect flow.handleConnectOauthActioncallssaveSecretdirectly, and there’s no sharedtry/catcharound theconnect_oauthbranch, so anEntitlementLimitErrorwill bubble out instead of returning the same 400-style response ashandleSaveAction.🤖 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/app/handlers/account-secrets.ts` around lines 228 - 341, handleConnectOauthAction currently calls saveSecret directly, so an EntitlementLimitError from the connect_oauth path will escape instead of being mapped to the same 400 response used in handleSaveAction. Wrap the access token and refresh token saveSecret calls in a try/catch inside handleConnectOauthAction, catch EntitlementLimitError specifically, and return the existing JSON error response format; keep the surrounding OAuth validation and token handling unchanged.
🧹 Nitpick comments (2)
packages/worker/src/package-runtime/package-workflows.ts (1)
718-721: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winWiden
envparam type instead of casting toEnv.
input.envis typed asPick<Env, 'APP_DB' | 'DYNAMIC_CALLABLE_WORKFLOWS'>, butgetWorkflowConcurrencyBackstopreadsWORKFLOW_CONCURRENT_LIMITfrom it, requiring theas Envcast on Line 770. This cast defeats type checking for the rest ofEnv's (likely much larger) surface and hides the function's true dependency onWORKFLOW_CONCURRENT_LIMITfrom its signature.♻️ Suggested fix
export async function createDynamicCallableWorkflow(input: { - env: Pick<Env, 'APP_DB' | 'DYNAMIC_CALLABLE_WORKFLOWS'> + env: Pick<Env, 'APP_DB' | 'DYNAMIC_CALLABLE_WORKFLOWS' | 'WORKFLOW_CONCURRENT_LIMIT'> userId: string userEmail?: string | null- fallbackLimit: getWorkflowConcurrencyBackstop(input.env as Env), + fallbackLimit: getWorkflowConcurrencyBackstop(input.env),Also applies to: 764-771
🤖 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/package-runtime/package-workflows.ts` around lines 718 - 721, `createDynamicCallableWorkflow` is casting `input.env` to `Env` because `getWorkflowConcurrencyBackstop` needs `WORKFLOW_CONCURRENT_LIMIT`; widen the `env` parameter type to include that key instead of using a cast. Update the `createDynamicCallableWorkflow` signature (and any related helper types like `getWorkflowConcurrencyBackstop`) so the dependency on `WORKFLOW_CONCURRENT_LIMIT` is explicit, and remove the `as Env` usage when reading it.packages/worker/src/package-runtime/workflow-statuses.ts (1)
12-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winConsider a compile-time exhaustiveness guard for the status partition.
activeWorkflowStatusValues/terminalWorkflowStatusValuescurrently cover the fullWorkflowRunStatusunion correctly (6+3=9), and this set is the exact input used by the entitlement service to count usage forconcurrent_workflows(perpackages/worker/src/entitlements/service.ts:171). Since the two lists are maintained independently from the union, an unnoticed future status addition (e.g. someone adds a new status but forgets to add it to either array) could silently under/over-count active workflows for quota enforcement, with no compile error to catch it.Consider adding a type-level exhaustiveness check, e.g.:
♻️ Suggested exhaustiveness guard
+type _AssertExhaustive< + T extends readonly WorkflowRunStatus[], +> = WorkflowRunStatus extends T[number] ? true : never + export const activeWorkflowStatusValues = [ 'queued', 'running', 'paused', 'waiting', 'waitingForPause', 'unknown', ] as const satisfies ReadonlyArray<WorkflowRunStatus> export const terminalWorkflowStatusValues = [ 'complete', 'errored', 'terminated', ] as const satisfies ReadonlyArray<WorkflowRunStatus> + +type _CheckPartitionIsExhaustive = _AssertExhaustive< + [...typeof activeWorkflowStatusValues, ...typeof terminalWorkflowStatusValues] +>🤖 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/package-runtime/workflow-statuses.ts` around lines 12 - 25, Add a compile-time exhaustiveness guard for the workflow status partition in the workflow-statuses module by tying activeWorkflowStatusValues and terminalWorkflowStatusValues back to the WorkflowRunStatus union so future status additions fail to compile unless they are assigned to exactly one list. Keep the existing arrays and the WorkflowRunStatus symbol as the source of truth, and introduce a type-level check near activeWorkflowStatusValues/terminalWorkflowStatusValues that validates the combined set covers the full union with no omissions.
🤖 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/entitlements/entitlements.node.test.ts`:
- Around line 270-321: The entitlement flow in sendOutboundEmail still splits
the limit check and counter update across assertWithinEntitlement() and
incrementDailyEntitlementCounter(), so concurrent requests can both pass before
either write lands. Refactor the email-sends-per-day path to use a single atomic
operation, ideally by combining the read-and-increment in one D1 transaction or
an equivalent conditional update helper, and update the daily counter test in
entitlements.node.test.ts to reflect the atomic behavior through the existing
assertWithinEntitlement and incrementDailyEntitlementCounter symbols.
In `@packages/worker/src/entitlements/service.ts`:
- Around line 219-249: The current assertWithinEntitlement flow is a
read-then-write check that can be bypassed by concurrent requests, so move
entitlement validation and consumption into one atomic operation. Update
assertWithinEntitlement and the related call sites to use a single per-resource
reservation/transaction path, similar to the D1 rate-limiter pattern, so current
usage is checked and incremented together before returning success.
---
Outside diff comments:
In `@packages/worker/src/app/handlers/account-secrets.ts`:
- Around line 228-341: handleConnectOauthAction currently calls saveSecret
directly, so an EntitlementLimitError from the connect_oauth path will escape
instead of being mapped to the same 400 response used in handleSaveAction. Wrap
the access token and refresh token saveSecret calls in a try/catch inside
handleConnectOauthAction, catch EntitlementLimitError specifically, and return
the existing JSON error response format; keep the surrounding OAuth validation
and token handling unchanged.
In `@packages/worker/src/jobs/service.ts`:
- Around line 924-936: The quota check in createJob is race-prone because
assertWithinEntitlement runs before insertJobRow with no atomic tie between
them. Update createJob so the scheduled_jobs entitlement check and job insertion
happen atomically, either by moving the limit enforcement into the same DB
transaction used for insertJobRow or by enforcing the quota directly at the
database layer. Use createJob, assertWithinEntitlement, and insertJobRow as the
key entry points when making the fix.
---
Nitpick comments:
In `@packages/worker/src/package-runtime/package-workflows.ts`:
- Around line 718-721: `createDynamicCallableWorkflow` is casting `input.env` to
`Env` because `getWorkflowConcurrencyBackstop` needs
`WORKFLOW_CONCURRENT_LIMIT`; widen the `env` parameter type to include that key
instead of using a cast. Update the `createDynamicCallableWorkflow` signature
(and any related helper types like `getWorkflowConcurrencyBackstop`) so the
dependency on `WORKFLOW_CONCURRENT_LIMIT` is explicit, and remove the `as Env`
usage when reading it.
In `@packages/worker/src/package-runtime/workflow-statuses.ts`:
- Around line 12-25: Add a compile-time exhaustiveness guard for the workflow
status partition in the workflow-statuses module by tying
activeWorkflowStatusValues and terminalWorkflowStatusValues back to the
WorkflowRunStatus union so future status additions fail to compile unless they
are assigned to exactly one list. Keep the existing arrays and the
WorkflowRunStatus symbol as the source of truth, and introduce a type-level
check near activeWorkflowStatusValues/terminalWorkflowStatusValues that
validates the combined set covers the full union with no omissions.
🪄 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: a802bb4b-f6d7-40be-bfcf-7e725b0d43f9
📒 Files selected for processing (38)
docs/contributing/architecture/entitlements.mddocs/contributing/architecture/index.mddocs/contributing/architecture/primitives.yamlpackages/worker/migrations/0048-user-plans-and-entitlement-counters.sqlpackages/worker/src/app/account-deletion.node.test.tspackages/worker/src/app/account-deletion.tspackages/worker/src/app/handlers/account-secrets.tspackages/worker/src/email/outbound.tspackages/worker/src/email/outbound.workers.test.tspackages/worker/src/email/test-schema.tspackages/worker/src/entitlements/entitlements.node.test.tspackages/worker/src/entitlements/errors.tspackages/worker/src/entitlements/plans.tspackages/worker/src/entitlements/service.tspackages/worker/src/entitlements/test-schema.tspackages/worker/src/jobs/service.node.test.tspackages/worker/src/jobs/service.tspackages/worker/src/mcp/capabilities/email/email-reply.tspackages/worker/src/mcp/capabilities/email/email-send.tspackages/worker/src/mcp/capabilities/packages/save-package-entitlements.node.test.tspackages/worker/src/mcp/capabilities/packages/save-package.tspackages/worker/src/mcp/capabilities/repo/repo-open-session.node.test.tspackages/worker/src/mcp/capabilities/repo/repo-open-session.tspackages/worker/src/mcp/capabilities/secrets/secret-set.tspackages/worker/src/mcp/capabilities/services/service-start.node.test.tspackages/worker/src/mcp/capabilities/services/service-start.tspackages/worker/src/mcp/capabilities/services/shared.tspackages/worker/src/mcp/generated-ui-api.tspackages/worker/src/mcp/run-codemode-registry.tspackages/worker/src/mcp/secrets/service.node.test.tspackages/worker/src/mcp/secrets/service.tspackages/worker/src/package-registry/service.node.test.tspackages/worker/src/package-registry/service.tspackages/worker/src/package-runtime/package-app.tspackages/worker/src/package-runtime/package-workflows.node.test.tspackages/worker/src/package-runtime/package-workflows.tspackages/worker/src/package-runtime/workflow-statuses.tspackages/worker/src/repo/external-publish.ts
|
🔎 Preview deployed: https://kody-pr-619.kentcdodds.workers.dev Worker: Mocks:
|
consumeDailyEntitlement checks the plan limit and increments the daily counter in a single conditional D1 upsert, closing the check-then-increment race Bugbot and CodeRabbit flagged on the email send path. The UTC day key is evaluated once per consumption, so a send near midnight can no longer check one day's row and increment another's. Plan-less users still consume uncapped so counters accumulate for later plan assignment. Row-count limits intentionally remain check-then-insert; documented as an accepted denial-of-wallet trade-off in entitlements.md. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
… no run) Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Resolutions: - users.plan now comes from main's migration 0046 (invite signup); dropped the duplicate ALTER TABLE from migration 0048, which keeps only the entitlement_daily_counters table - sendOutboundEmail: adopted main's required accountEmail (verified-account gate) as the entitlement plan-lookup email; removed the interim optional userEmail parameter - package-workflows: kept the entitlements concurrency path alongside main's usage metering - entitlement test schema now mirrors users.email_verified_at as well Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Migration 0046 (invite signup, now on main) adds the column; 0048 keeps only the entitlement_daily_counters table. Doc updated to match. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/migrations/0048-user-plans-and-entitlement-counters.sql`:
- Around line 3-4: The schema-history note in test-schema.ts is stale: the
users.plan column is introduced by 0046-invites-email-verification.sql, not
0048-user-plans-and-entitlement-counters.sql. Update the mapping/comment in the
schema history entry referenced by test-schema.ts so it points to 0046, and keep
the rest of the entitlement history notes aligned with the actual migration that
added users.plan.
🪄 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: a7f190d0-23bc-4537-8fdf-6b8221725142
📒 Files selected for processing (16)
docs/contributing/architecture/entitlements.mddocs/contributing/architecture/index.mddocs/contributing/architecture/primitives.yamlpackages/worker/migrations/0048-user-plans-and-entitlement-counters.sqlpackages/worker/src/app/account-deletion.node.test.tspackages/worker/src/app/account-deletion.tspackages/worker/src/email/outbound.tspackages/worker/src/email/outbound.workers.test.tspackages/worker/src/entitlements/entitlements.node.test.tspackages/worker/src/entitlements/service.tspackages/worker/src/entitlements/test-schema.tspackages/worker/src/jobs/service.node.test.tspackages/worker/src/jobs/service.tspackages/worker/src/mcp/run-codemode-registry.tspackages/worker/src/package-runtime/package-workflows.node.test.tspackages/worker/src/package-runtime/package-workflows.ts
✅ Files skipped from review due to trivial changes (3)
- docs/contributing/architecture/index.md
- docs/contributing/architecture/primitives.yaml
- docs/contributing/architecture/entitlements.md
🚧 Files skipped from review as they are similar to previous changes (9)
- packages/worker/src/mcp/run-codemode-registry.ts
- packages/worker/src/app/account-deletion.ts
- packages/worker/src/entitlements/test-schema.ts
- packages/worker/src/jobs/service.ts
- packages/worker/src/jobs/service.node.test.ts
- packages/worker/src/email/outbound.workers.test.ts
- packages/worker/src/package-runtime/package-workflows.node.test.ts
- packages/worker/src/package-runtime/package-workflows.ts
- packages/worker/src/entitlements/entitlements.node.test.ts
| -- The nullable users.plan column is added by migration | ||
| -- 0046-invites-email-verification.sql (NULL means legacy/unlimited). |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm which migration actually adds the users.plan column
rg -n --iglob '*.sql' -e 'ADD COLUMN[[:space:]]+plan' -e 'plan[[:space:]]+TEXT' packages/worker/migrations
echo '--- 0046 header/plan refs ---'
fd '0046' packages/worker/migrations --exec rg -n 'plan|email_verified_at' {}Repository: kentcdodds/kody
Length of output: 489
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- packages/worker/migrations/0048-user-plans-and-entitlement-counters.sql ---'
cat -n packages/worker/migrations/0048-user-plans-and-entitlement-counters.sql
echo '--- packages/worker/src/entitlements/test-schema.ts (relevant lines) ---'
rg -n -A4 -B4 '0046|0048|plan|email_verified_at|entitlement_daily_counters' packages/worker/src/entitlements/test-schema.tsRepository: kentcdodds/kody
Length of output: 2451
Update the schema-history note in packages/worker/src/entitlements/test-schema.ts. 0046-invites-email-verification.sql adds users.plan, so the 0048 mapping there is stale and should point to 0046 instead.
🤖 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/migrations/0048-user-plans-and-entitlement-counters.sql`
around lines 3 - 4, The schema-history note in test-schema.ts is stale: the
users.plan column is introduced by 0046-invites-email-verification.sql, not
0048-user-plans-and-entitlement-counters.sql. Update the mapping/comment in the
schema history entry referenced by test-schema.ts so it points to 0046, and keep
the rest of the entitlement history notes aligned with the actual migration that
added users.plan.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d88dbd3. Configure here.
| current, | ||
| upgradeHint: buildEntitlementUpgradeHint(input.resource), | ||
| }) | ||
| } |
There was a problem hiding this comment.
Stale runs block service restarts
Medium Severity
package_services enforcement counts distinct recent D1 rows with status = 'running', then rejects when current + 1 exceeds the plan limit. Stale running rows for services that are already stopped on the Durable Object still count toward that total, so a user at the cap can be denied when restarting a stopped service (or see quota “full” with no services actually running) until those rows age out or are cleaned up.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit d88dbd3. Configure here.
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>


Summary
Denial-of-wallet protection ahead of open signup: per-user plans with per-plan resource limits, enforced through one shared helper with one typed error shape. No payment code — Stripe comes later, after metering data informs the quota numbers. This builds the enforcement machinery only.
Hard invariant preserved: users with a NULL plan are unlimited. Enforcement (including every counting query) short-circuits before doing any work for plan-less users, so nothing changes for existing accounts. Enforcement activates only when
users.planis set to a known plan name.The shared primitive (
packages/worker/src/entitlements/)plans.ts— plan names (partner,personal,pro; NULL = legacy/unlimited), thePlanLimitsconfig per plan (max saved packages, scheduled jobs, package services + persistent-mode allowance, repo sessions, email sends/day, secrets, storage bytes, concurrent workflows), and the resource registry. Limit numbers are conservative placeholders to be tuned once metering lands.errors.ts—EntitlementLimitErrorwith the single details contract{ code: 'entitlement_limit_exceeded', resource, plan, limit, current, upgradeHint }and the single user-facing message builder used by every enforcement point across MCP and UI surfaces.service.ts—getUserPlan(email-hash-verified lookup ofusers.plan),assertWithinEntitlement(the one helper every row-count enforcement point calls),consumeDailyEntitlement(atomic check-and-increment for rate-style limits), and built-in D1 usage counters per resource.docs/contributing/architecture/entitlements.md— module contract, error shape, counting strategy, concurrency trade-offs, and how to add an enforcement point.Enforcement points (one per resource, identical pattern)
scheduled_jobscreateJob(jobs/service.ts)saved_packagespackage_savecreate branch + projection insertpackage_servicesservice_startcapabilitypersistent_package_servicesservice_startformode: 'persistent'repo_sessionsrepo_open_sessionnew-session pathemail_sends_per_daysendOutboundEmailconsumeDailyEntitlement; counter increments for every user on every attempt;email_sendandemail_replywiredsecretssaveSecretnew-entry branchsecret_set, account secrets UI, and generated-UI APIconcurrent_workflowscreateDynamicCallableWorkflowWORKFLOW_CONCURRENT_LIMITenv var absorbed as the NULL-plan backstop (same numeric behavior as before, new uniform error)storage_bytesConcurrency notes (review feedback addressed)
consumeDailyEntitlement), so concurrent sends cannot race past the cap, and the UTC day key is evaluated once per consumption.entitlements.md.Migration 0048 + coordination notes (for parallel branches)
0048-user-plans-and-entitlement-counters.sqladds nullableusers.planand theentitlement_daily_counterstable.users.plancolumn in migration 0046 (ALTER TABLE users ADD COLUMN plan TEXT DEFAULT NULL). Whichever lands second must drop its duplicateALTER TABLEat rebase — two migrations must never add the same column. Renumber 0048 if the prefix is taken by then.entitlement_daily_countersis included in the account-deletion cascade (account-deletion.ts); main's storage-inventory rework (Make account deletion cover user storage inventory #614) has been merged into this branch.Plan lookup and documented fail-open gaps
The MCP
userIdis the SHA-256 of the normalized account email, so plan lookup goes through the email and verifies the hash matches before touching D1. Code paths without a verified account email fail open to unlimited (or to the global backstop, for workflows). These gaps are deliberate v1 scope, documented in the architecture doc:serviceStartand DO auto-start/alarm restartssyncArtifactSourceSnapshotAll are only reachable after a user action that is itself gated.
Testing
npm run validatefully green locally (format, lint, typecheck, unit, Playwright E2E, MCP E2E).System recap — adds a new primitive (high risk)
Mode: recap · Base:
main@7019c1d· Head:50c50b9Classification: adds — new
entitlementsprimitive (plans + quota enforcement);primitives.yamlupdated in this PR.Primitives touched
entitlementsd1-app-dbusers.plan,entitlement_daily_countersjobscreateJobcallsassertWithinEntitlementsaved-packagespackage-servicesservice_startgated (persistent mode + count)repo-sessionsemailsecretsworkflowsapp-uimcp-serveruserEmailSystem map
Before / after
Invariants
per-user-isolation: all plan lookups verifysha256(email) === userIdbefore readingusers.plan; every counting query filters byuser_id. No cross-user reads added.Summary by CodeRabbit
New Features
Bug Fixes
Documentation