[codex] Add user suspension guard#462
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds Convex-backed user suspensions, server-side gating for cost-incurring requests, fraud-webhook wiring to upsert suspensions, extracts Stripe→WorkOS customer resolver, and updates UI/error messages and tests. ChangesUser Suspension Infrastructure
Suspension gating and messaging
Fraud Webhook and Customer Resolution
Request Gating Integration
Sequence Diagram(s)sequenceDiagram
participant Client
participant ChatHandler as Chat Handler
participant assertGate as assertUserCanMakeCostIncurringRequest
participant Convex as Convex Service
participant ErrorSDK as ChatSDKError
Client->>ChatHandler: POST /api/chat
ChatHandler->>ChatHandler: getUserIDAndPro(userId)
ChatHandler->>assertGate: assertUserCanMakeCostIncurringRequest(userId)
assertGate->>Convex: api.userSuspensions.getActiveByUser(userId)
alt User is Suspended
Convex-->>assertGate: suspension record
assertGate->>ErrorSDK: create forbidden:chat error with message+metadata
ErrorSDK-->>ChatHandler: throw 403 ChatSDKError
ChatHandler-->>Client: 403 Forbidden
else User is Not Suspended
Convex-->>assertGate: null
assertGate-->>ChatHandler: allow
ChatHandler->>ChatHandler: checkRateLimit()
ChatHandler->>ChatHandler: Stream response
ChatHandler-->>Client: 200 OK stream
end
sequenceDiagram
participant Stripe as Stripe Webhook
participant FraudRoute as Fraud Webhook Route
participant blockUser as blockFraudulentUser
participant suspendHelper as suspendCustomerUsers
participant ResolveCust as resolveUserIdsFromCustomer
participant Convex as Convex userSuspensions
Stripe->>FraudRoute: early_fraud_warning / dispute_created
FraudRoute->>blockUser: construct suspension metadata
blockUser->>blockUser: cancel subscriptions + detach payments + mark blocked
alt billing hold / non-fraudulent dispute
blockUser->>suspendHelper: invoke with category="dispute_billing_hold"
else fraudulent dispute or early warning
blockUser->>suspendHelper: invoke with appropriate category
end
suspendHelper->>ResolveCust: resolveUserIdsFromCustomer(stripeCustomerId)
ResolveCust-->>suspendHelper: {userIds, orgId}
suspendHelper->>Convex: upsertActive(userId, suspension data)
Convex-->>suspendHelper: inserted/patched suspension
suspendHelper-->>blockUser: done
blockUser-->>FraudRoute: blocking complete
FraudRoute->>Stripe: finalize webhook
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
lib/suspensions.ts (1)
8-14: ⚡ Quick winFail fast on missing
CONVEX_SERVICE_ROLE_KEY.Using a non-null assertion here defers misconfiguration to runtime request handling. Prefer explicit startup-time validation so failures are immediate and diagnosable.
Suggested change
-const serviceKey = process.env.CONVEX_SERVICE_ROLE_KEY!; +const serviceKey = process.env.CONVEX_SERVICE_ROLE_KEY; +if (!serviceKey) { + throw new Error("Missing CONVEX_SERVICE_ROLE_KEY"); +}🤖 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 `@lib/suspensions.ts` around lines 8 - 14, The code uses a non-null assertion on process.env.CONVEX_SERVICE_ROLE_KEY (serviceKey) inside getActiveSuspensionForUser which delays misconfiguration errors to runtime; instead validate and fail fast at module initialization: read and check CONVEX_SERVICE_ROLE_KEY once (e.g., throw a clear Error if missing) before exporting getActiveSuspensionForUser, and keep the function using that validated serviceKey when calling getConvexClient().query(api.userSuspensions.getActiveByUser, { serviceKey, userId }); Ensure the thrown error message names CONVEX_SERVICE_ROLE_KEY for easy diagnosis.
🤖 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 `@convex/userSuspensions.ts`:
- Around line 19-31: The getActiveByUser implementation loads all active
suspensions into memory and sorts them; change the query to return a bounded
result: use ctx.db.query("user_suspensions").withIndex("by_user_and_status",
...) and apply .order("desc", "source_created_at") (or equivalent) then .take(1)
instead of .collect(), or if the index lacks source_created_at, add
source_created_at to the by_user_and_status index and then use
.order("desc").take(1) to fetch the latest suspension without in-memory sorting.
In `@lib/billing/resolve-customer-users.ts`:
- Around line 22-34: The current call to
workos.userManagement.listOrganizationMemberships in resolve-customer-users.ts
only reads the first page; change the logic to iterate all pages (e.g., use
memberships.autoPagination) to collect every membership.userId into userIds
before returning, using logPrefix and orgId as before; if userIds is empty log
the same error and return { userIds, orgId }. Ensure you reference
memberships.autoPagination from the listOrganizationMemberships result (or
implement manual pagination with list_metadata.after) so no active members are
missed.
---
Nitpick comments:
In `@lib/suspensions.ts`:
- Around line 8-14: The code uses a non-null assertion on
process.env.CONVEX_SERVICE_ROLE_KEY (serviceKey) inside
getActiveSuspensionForUser which delays misconfiguration errors to runtime;
instead validate and fail fast at module initialization: read and check
CONVEX_SERVICE_ROLE_KEY once (e.g., throw a clear Error if missing) before
exporting getActiveSuspensionForUser, and keep the function using that validated
serviceKey when calling
getConvexClient().query(api.userSuspensions.getActiveByUser, { serviceKey,
userId }); Ensure the thrown error message names CONVEX_SERVICE_ROLE_KEY for
easy diagnosis.
🪄 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
Run ID: 89425ba6-0aa9-4c1d-98fa-ec2ec93b60e8
⛔ Files ignored due to path filters (1)
convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (13)
app/api/agent-long/route.tsapp/api/fraud/webhook/route.tsapp/api/subscription/webhook/route.tsconvex/__tests__/userSuspensions.test.tsconvex/schema.tsconvex/userSuspensions.tslib/__tests__/suspensionMessage.test.tslib/__tests__/suspensions.test.tslib/api/chat-handler.tslib/billing/resolve-customer-users.tslib/suspensionMessage.tslib/suspensions.tstrigger/agent-long.ts
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 `@app/components/MessageErrorState.tsx`:
- Around line 145-150: The Button's onClick opens an external support page with
window.open without protecting window.opener; update the handler in the
component using the Button (the onClick passed to the Button in
MessageErrorState) to open the URL with noopener and noreferrer protection —
e.g. call window.open("https://help.hackerai.co/", "_blank",
"noopener,noreferrer") or replace the Button with an anchor element that has
target="_blank" and rel="noopener noreferrer" to ensure the new tab cannot
access window.opener.
🪄 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
Run ID: bd6a26b1-211e-4ad2-a078-48aaf354e06b
📒 Files selected for processing (2)
app/components/MessageErrorState.tsxapp/components/chat.tsx
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 `@lib/__tests__/resolve-customer-users.test.ts`:
- Around line 23-26: The test setup spies on console.error in the beforeEach
block but never restores it, which can leak mocked global state; add an
afterEach that restores the spy (either call jest.restoreAllMocks() in afterEach
or keep a reference to the spy returned by jest.spyOn(console, "error") and call
mockRestore() on it) so console.error is reset after each test; update the
beforeEach/afterEach surrounding the jest.spyOn(console, "error") invocation to
perform this restore.
🪄 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
Run ID: 395758b2-ec11-414f-a433-298236ebb05a
📒 Files selected for processing (6)
app/components/MessageErrorState.tsxconvex/__tests__/userSuspensions.test.tsconvex/schema.tsconvex/userSuspensions.tslib/__tests__/resolve-customer-users.test.tslib/billing/resolve-customer-users.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- convex/schema.ts
- app/components/MessageErrorState.tsx
- convex/userSuspensions.ts
- lib/billing/resolve-customer-users.ts
- convex/tests/userSuspensions.test.ts
Summary
Adds a first-class Convex-backed suspension guard for payment risk events and wires it into chat and agent request entry points.
Changes
user_suspensionsschema and Convex mutations/queries for active and resolved suspension state.dispute_billing_holdso the category describes app policy rather than raw Stripe reason.Impact
Users with active fraud warnings, fraudulent disputes, or disputed-payment billing holds are blocked before model work starts, including when they would otherwise appear as free users. Billing/support paths can still use Stripe metadata and Convex state to understand why an account is held.
Validation
tsc --noEmitSummary by CodeRabbit
New Features
UI
Tests