Skip to content

fix: canonicalize subscription entitlements - #134

Open
Melvynx wants to merge 1 commit into
mainfrom
security/saveit-weekly-audit-2026-08-03
Open

fix: canonicalize subscription entitlements#134
Melvynx wants to merge 1 commit into
mainfrom
security/saveit-weekly-audit-2026-08-03

Conversation

@Melvynx

@Melvynx Melvynx commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixes stale subscription entitlement selection: newer cancellation/downgrade rows now beat older active/pro rows when deriving limits.
  • Preserves manual lifetime grants as durable Pro entitlements.
  • Applies canonical subscription selection across API keys, bookmark creation/processing/export, chat limits, subscription queries, and user limit/onboarding state.
  • Adds regression tests for canonical subscription behavior and entitlement call-site guards.

Test Plan

  • ulimit -v unlimited; pnpm --filter @workspace/backend exec vitest run convex/billing/plans.test.ts --reporter=verbose - passed, 13 tests
  • git diff --cached --check - passed before commit
  • Added-line static security scan - clean
  • Independent reviewer - approved after requested fixes

Risk/Rollback

  • Risk: users relying on stale older active rows after cancellation/downgrade will correctly lose Pro limits. Manual lifetime grants remain preserved.
  • Rollback: revert this PR, but that reopens stale-row entitlement grants.

Known baseline failures

  • Broad backend tsc --noEmit attempts produced existing workspace/module-resolution noise across extensions/mobile/UI packages, not tied to this Convex hotfix.
  • pnpm --filter @workspace/backend test -- convex/billing/plans.test.ts also collected unrelated backend suites and hit existing convex/values module-resolution failures; the targeted vitest run convex/billing/plans.test.ts command passed.

@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
saveit-now-web Ready Ready Preview Aug 3, 2026 8:54am

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c5e28136c1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +78 to +80
function subscriptionTimestamp(subscription: SubscriptionPlanState): number {
return subscription.updatedAt ?? subscription.createdAt ?? 0;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid using maintenance timestamps to order entitlements

When a user has multiple subscription rows and an older row is repaired, migration/repair_subscriptions.ts sets that row's updatedAt to Date.now() even when merely correcting its customer ID; updateFromWebhook similarly timestamps updates by processing time. This selector therefore makes that historical row canonical over a genuinely newer subscription, so an old canceled row can revoke an active payer's Pro access or an old active row can restore access after cancellation. Use a billing-lifecycle/event ordering field, or keep non-lifecycle maintenance from changing the field used here.

Useful? React with 👍 / 👎.

Comment on lines 12 to +14
.withIndex("by_user", (q) => q.eq("userId", userId))
.first();
.order("desc")
.take(20);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include lifetime grants outside the recent-row window

When an account has more than 20 subscription rows, this newest-by-creation window can exclude its durable manual lifetime grant. In particular, grantLifetimeProByEmail patches the oldest by_user row, while all new entitlement readers take only the 20 newest rows before canonicalization; such a user is then treated as free for checkout, quotas, API access, and export despite the helper's lifetime-wins invariant. Query lifetime status separately or use an indexed selection that cannot truncate the winning row.

Useful? React with 👍 / 👎.

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.

1 participant