Skip to content

Per-user plans and entitlement/quota enforcement - #619

Merged
kody-bot merged 8 commits into
mainfrom
cursor/user-plan-entitlements-de2e
Jul 5, 2026
Merged

kody-bot merged 8 commits into
mainfrom
cursor/user-plan-entitlements-de2e

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 5, 2026 •

Copy link
Copy Markdown
Owner

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.plan is set to a known plan name.

The shared primitive (packages/worker/src/entitlements/)

  • plans.ts — plan names (partner, personal, pro; NULL = legacy/unlimited), the PlanLimits config 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 — EntitlementLimitError with 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 of users.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)

Resource Enforcement point Notes
scheduled_jobs createJob (jobs/service.ts) Exemplar implementation
saved_packages package_save create branch + projection insert Updates/replaces not gated
package_services service_start capability Already-running restart exempt
persistent_package_services service_start for mode: 'persistent' Boolean gate (limit 0 vs unlimited)
repo_sessions repo_open_session new-session path Resume exempt
email_sends_per_day sendOutboundEmail Atomic consumeDailyEntitlement; counter increments for every user on every attempt; email_send and email_reply wired
secrets saveSecret new-entry branch Wired from MCP secret_set, account secrets UI, and generated-UI API
concurrent_workflows createDynamicCallableWorkflow WORKFLOW_CONCURRENT_LIMIT env var absorbed as the NULL-plan backstop (same numeric behavior as before, new uniform error)
storage_bytes not yet enforced Config defined for later

Concurrency notes (review feedback addressed)

  • The daily email limit is enforced with a single conditional D1 upsert (consumeDailyEntitlement), so concurrent sends cannot race past the cap, and the UTC day key is evaluated once per consumption.
  • Row-count limits (packages, jobs, sessions, secrets, workflows) intentionally remain check-then-insert: a concurrent burst can overshoot by a few rows before the next check. These are order-of-magnitude denial-of-wallet caps, not billing-grade accounting; making each insert conditional would couple the module to every resource's write path. Documented in entitlements.md.

Migration 0048 + coordination notes (for parallel branches)

Plan lookup and documented fail-open gaps

The MCP userId is 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:

  • repo-session external publish creating a first saved-package projection (no email in that path)
  • package-app runtime bridge serviceStart and DO auto-start/alarm restarts
  • artifact-sync sessions opened by syncArtifactSourceSnapshot
  • workflow-internal contexts spawning more workflows (these still hit the global backstop, as before)

All are only reachable after a user action that is itself gated.

Testing

  • npm run validate fully green locally (format, lint, typecheck, unit, Playwright E2E, MCP E2E).
  • New unit coverage: entitlements module (13 tests), plus per-enforcement-point tests asserting (a) plan user at limit is denied with the exact typed details, (b) NULL-plan user unaffected, (c) create-vs-update / resume / already-running exemptions, (d) atomic daily consumption denies at the cap without advancing the counter.
  • Email daily limit covered by workers tests against real miniflare D1 with migrations applied.
System recap — adds a new primitive (high risk)

Mode: recap · Base: main @ 7019c1d · Head: 50c50b9

Classification: adds — new entitlements primitive (plans + quota enforcement); primitives.yaml updated in this PR.

Primitives touched

Primitive Group Impact
entitlements auth adds — new module, error shape, and enforcement helpers
d1-app-db storage extends — migration 0048: users.plan, entitlement_daily_counters
jobs assistant composes — createJob calls assertWithinEntitlement
saved-packages assistant composes — create paths gated
package-services assistant composes — service_start gated (persistent mode + count)
repo-sessions runtime composes — new-session open gated
email assistant composes — atomic daily send limit
secrets assistant composes — new-entry creation gated
workflows assistant extends — concurrency limit moved into entitlements path (env var becomes NULL-plan backstop; error shape changed)
app-ui surfaces composes — account secrets handler passes account email
mcp-server surfaces composes — capabilities thread userEmail

System map

flowchart LR
	entitlements["entitlements"]:::added
	d1AppDb["d1-app-db"]:::extended
	workflows["workflows"]:::extended
	jobs["jobs"]:::touched
	savedPackages["saved-packages"]:::touched
	packageServices["package-services"]:::touched
	repoSessions["repo-sessions"]:::touched
	email["email"]:::touched
	secrets["secrets"]:::touched
	mcpServer["mcp-server"]:::touched
	appUi["app-ui"]:::touched
	rbac["rbac"]:::untouched
	mcpServer --> jobs & savedPackages & packageServices & repoSessions & email & secrets & workflows
	appUi --> secrets
	jobs & savedPackages & packageServices & repoSessions & email & secrets & workflows --> entitlements
	entitlements --> d1AppDb
	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

Before / after

before: users(id, username, email, password_hash, created_at, updated_at)
after:  users(..., plan TEXT NULL)  -- NULL = legacy/unlimited

new:    entitlement_daily_counters(user_id, resource, day, count, updated_at)
        PRIMARY KEY (user_id, resource, day)
before: workflows.create limit error = plain Error('workflows.create would exceed …')
after:  EntitlementLimitError { code: 'entitlement_limit_exceeded',
        resource: 'concurrent_workflows', plan: null, limit: 100, current, upgradeHint }

Invariants

  • per-user-isolation: all plan lookups verify sha256(email) === userId before reading users.plan; every counting query filters by user_id. No cross-user reads added.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added per-user plan and quota enforcement across email sending, job creation, saved packages, repo sessions, services, secrets, and workflow creation.
    • Expanded support for legacy accounts with no plan, which continue to work without limits.
  • Bug Fixes

    • Improved account deletion to remove usage counters tied to the deleted account.
    • Standardized limit errors and messages for clearer user-facing feedback.
  • Documentation

    • Added architecture guidance for entitlement behavior, limits, and enforcement rules.

cursoragent and others added 2 commits July 5, 2026 16:55
…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>
@coderabbitai

coderabbitai Bot commented Jul 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a plan-based entitlements system (packages/worker/src/entitlements/) with users.plan and entitlement_daily_counters schema, a shared enforcement service (assertWithinEntitlement, consumeDailyEntitlement), and a standardized EntitlementLimitError. Enforcement is wired into email sending, secrets, saved packages, scheduled jobs, repo sessions, package services, and workflow concurrency, with userEmail threaded through relevant call sites, account-deletion cascade support, and architecture documentation.

Changes

Entitlements system

Layer / File(s) Summary
Core entitlement contracts, service, and tests
packages/worker/src/entitlements/plans.ts, errors.ts, service.ts, test-schema.ts, entitlements.node.test.ts, packages/worker/migrations/0048-*.sql
Defines plan names/limits, EntitlementLimitError, getUserPlan, assertWithinEntitlement, consumeDailyEntitlement, daily counters, workflow concurrency backstop, test schema helper, and unit tests.
Architecture documentation
docs/contributing/architecture/entitlements.md, index.md, primitives.yaml
Adds/links the entitlements architecture doc covering plan semantics, error contract, counting strategy, and enforcement checklist.
Account deletion cascade
packages/worker/src/app/account-deletion.ts, account-deletion.node.test.ts
Adds entitlement_daily_counters to the user-scoped cascade deletion list and tests row removal.
Email sending enforcement
packages/worker/src/email/outbound.ts, outbound.workers.test.ts, test-schema.ts
Enforces email_sends_per_day via consumeDailyEntitlement before sending and adds entitlement tests.
Secrets enforcement and userEmail plumbing
packages/worker/src/app/handlers/account-secrets.ts, mcp/capabilities/secrets/secret-set.ts, mcp/generated-ui-api.ts, mcp/secrets/service.ts, service.node.test.ts
Threads userEmail into saveSecret call sites and gates secret creation with assertWithinEntitlement.
Saved package enforcement
mcp/capabilities/packages/save-package.ts, save-package-entitlements.node.test.ts, package-registry/service.ts, service.node.test.ts
Gates saved-package creation with assertWithinEntitlement for saved_packages.
Scheduled jobs enforcement
packages/worker/src/jobs/service.ts, service.node.test.ts
Gates createJob with assertWithinEntitlement for scheduled_jobs.
Repo session and package service enforcement
mcp/capabilities/repo/repo-open-session.ts, mcp/capabilities/services/service-start.ts, shared.ts, related tests
Gates repo session opening and service starts, adds resolveDeclaredPackageService.
Workflow concurrency refactor
package-runtime/workflow-statuses.ts, package-workflows.ts, package-app.ts, mcp/run-codemode-registry.ts, repo/external-publish.ts, tests
Replaces DB-count concurrency checks with assertWithinEntitlement/getWorkflowConcurrencyBackstop and threads userEmail.

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
Loading

Possibly related PRs

  • kentcdodds/kody#177: Both PRs modify the saveSecret create path in packages/worker/src/mcp/secrets/service.ts.
  • kentcdodds/kody#403: Both PRs modify createDynamicCallableWorkflow in package-runtime/package-workflows.ts.
  • kentcdodds/kody#418: Both PRs extend packages/worker/src/app/account-deletion.ts's userScopedTables cascade deletion list.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: adding per-user plans and entitlement/quota enforcement.
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.
✨ 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/user-plan-entitlements-de2e

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.

Comment thread packages/worker/src/email/outbound.ts
Comment thread packages/worker/src/mcp/capabilities/packages/save-package.ts
…rage)

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

@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: 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 lift

Atomic quota enforcement needed
Two concurrent createJob calls can still bypass scheduled_jobs: assertWithinEntitlement reads the current count before insertJobRow, 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 win

Wrap saveSecret in the OAuth connect flow. handleConnectOauthAction calls saveSecret directly, and there’s no shared try/catch around the connect_oauth branch, so an EntitlementLimitError will bubble out instead of returning the same 400-style response as handleSaveAction.

🤖 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 win

Widen env param type instead of casting to Env.

input.env is typed as Pick<Env, 'APP_DB' | 'DYNAMIC_CALLABLE_WORKFLOWS'>, but getWorkflowConcurrencyBackstop reads WORKFLOW_CONCURRENT_LIMIT from it, requiring the as Env cast on Line 770. This cast defeats type checking for the rest of Env's (likely much larger) surface and hides the function's true dependency on WORKFLOW_CONCURRENT_LIMIT from 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 win

Consider a compile-time exhaustiveness guard for the status partition.

activeWorkflowStatusValues/terminalWorkflowStatusValues currently cover the full WorkflowRunStatus union correctly (6+3=9), and this set is the exact input used by the entitlement service to count usage for concurrent_workflows (per packages/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

📥 Commits

Reviewing files that changed from the base of the PR and between 3b1f3c4 and 019e99f.

📒 Files selected for processing (38)
  • docs/contributing/architecture/entitlements.md
  • docs/contributing/architecture/index.md
  • docs/contributing/architecture/primitives.yaml
  • packages/worker/migrations/0048-user-plans-and-entitlement-counters.sql
  • packages/worker/src/app/account-deletion.node.test.ts
  • packages/worker/src/app/account-deletion.ts
  • packages/worker/src/app/handlers/account-secrets.ts
  • packages/worker/src/email/outbound.ts
  • packages/worker/src/email/outbound.workers.test.ts
  • packages/worker/src/email/test-schema.ts
  • packages/worker/src/entitlements/entitlements.node.test.ts
  • packages/worker/src/entitlements/errors.ts
  • packages/worker/src/entitlements/plans.ts
  • packages/worker/src/entitlements/service.ts
  • packages/worker/src/entitlements/test-schema.ts
  • packages/worker/src/jobs/service.node.test.ts
  • packages/worker/src/jobs/service.ts
  • packages/worker/src/mcp/capabilities/email/email-reply.ts
  • packages/worker/src/mcp/capabilities/email/email-send.ts
  • packages/worker/src/mcp/capabilities/packages/save-package-entitlements.node.test.ts
  • packages/worker/src/mcp/capabilities/packages/save-package.ts
  • packages/worker/src/mcp/capabilities/repo/repo-open-session.node.test.ts
  • packages/worker/src/mcp/capabilities/repo/repo-open-session.ts
  • packages/worker/src/mcp/capabilities/secrets/secret-set.ts
  • packages/worker/src/mcp/capabilities/services/service-start.node.test.ts
  • packages/worker/src/mcp/capabilities/services/service-start.ts
  • packages/worker/src/mcp/capabilities/services/shared.ts
  • packages/worker/src/mcp/generated-ui-api.ts
  • packages/worker/src/mcp/run-codemode-registry.ts
  • packages/worker/src/mcp/secrets/service.node.test.ts
  • packages/worker/src/mcp/secrets/service.ts
  • packages/worker/src/package-registry/service.node.test.ts
  • packages/worker/src/package-registry/service.ts
  • packages/worker/src/package-runtime/package-app.ts
  • packages/worker/src/package-runtime/package-workflows.node.test.ts
  • packages/worker/src/package-runtime/package-workflows.ts
  • packages/worker/src/package-runtime/workflow-statuses.ts
  • packages/worker/src/repo/external-publish.ts

Comment thread packages/worker/src/entitlements/entitlements.node.test.ts
Comment thread packages/worker/src/entitlements/service.ts
@github-actions

github-actions Bot commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-619.kentcdodds.workers.dev

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

Mocks:

Comment thread packages/worker/src/email/outbound.ts
Comment thread packages/worker/src/jobs/service.ts
cursoragent and others added 5 commits July 5, 2026 17:40
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>

@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

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 019e99f and f2c265a.

📒 Files selected for processing (16)
  • docs/contributing/architecture/entitlements.md
  • docs/contributing/architecture/index.md
  • docs/contributing/architecture/primitives.yaml
  • packages/worker/migrations/0048-user-plans-and-entitlement-counters.sql
  • packages/worker/src/app/account-deletion.node.test.ts
  • packages/worker/src/app/account-deletion.ts
  • packages/worker/src/email/outbound.ts
  • packages/worker/src/email/outbound.workers.test.ts
  • packages/worker/src/entitlements/entitlements.node.test.ts
  • packages/worker/src/entitlements/service.ts
  • packages/worker/src/entitlements/test-schema.ts
  • packages/worker/src/jobs/service.node.test.ts
  • packages/worker/src/jobs/service.ts
  • packages/worker/src/mcp/run-codemode-registry.ts
  • packages/worker/src/package-runtime/package-workflows.node.test.ts
  • packages/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

Comment on lines +3 to +4
-- The nullable users.plan column is added by migration
-- 0046-invites-email-verification.sql (NULL means legacy/unlimited).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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.ts

Repository: 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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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),
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d88dbd3. Configure here.

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