fix(billing): withhold the payer credit and storage pools from callers who cannot manage billing - #6567
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryHigh Risk Overview Adds server-side Makes Reviewed by Cursor Bugbot for commit 79a3486. Configure here. |
Greptile SummaryThis PR prevents callers without billing-management authority from receiving payer-level pooled credit and storage data while preserving workspace billing standing.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/billing/application/get-billing-status.ts | Gates payer-pool projection by acting-principal billing authority and defers storage-pool resolution until disclosure is authorized. |
| apps/sim/lib/billing/core/workspace-billing-authority.ts | Implements the existing billing-management policy using canonical workspace payer and organization membership data. |
| apps/sim/lib/api/contracts/v2/billing.ts | Makes payer credit and storage fields nullable and documents their authorization semantics. |
| apps/docs/openapi-v2-billing.json | Regenerates the public billing-status schema to represent withheld payer pools as null. |
| apps/sim/lib/billing/application/billing-use-cases.test.ts | Covers authorized and unauthorized human callers, actor-less workspace keys, standing visibility, and deferred storage queries. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[GET billing status] --> B{Principal type}
B -->|Workspace API key| C[Resolve workspace billing status]
C --> D[Return plan, period, and standing]
D --> E[credits = null<br/>storage = null]
B -->|Personal API key| F{Requested workspace?}
F -->|No| G[Resolve caller's account scope]
G --> H[Return account credits and storage]
F -->|Yes| I[Resolve workspace and payer]
I --> J{Can manage payer billing?}
J -->|No| D
J -->|Yes| K[Resolve payer storage pool]
K --> L[Return pooled credits and storage]
Reviews (2): Last reviewed commit: "chore(billing): tidy payer-pool concealm..." | Re-trigger Greptile
`GET /api/v2/billing/status` resolved the workspace's payer and projected that payer's pooled allowances — credits used, credit limit, credits remaining, and the payer entity's storage usage and quota — to any caller holding only `read` on the workspace, including a personal API key. The payer pool is shared across every workspace that payer funds, and the platform already treats it as privileged: the workspace credit-availability surface computes `canViewPayerPool` from `canManageWorkspaceBilling` and substitutes member-scoped or null figures for everyone else. The new versioned endpoint had no equivalent gate. `credits` and `storage` are now projected only to a caller who may manage the resolved payer's billing: the billed account holder of a personally hosted workspace, an admin of the hosting organization, or a workspace API key, which only a workspace admin can provision. The endpoint stays at `read` so a plain member keeps the plan, period, and standing the workspace UI already shows them, and an exceeded pooled limit still reports as `limit_exceeded` without disclosing the numbers behind it. Both fields are nullable on the wire and in the regenerated OpenAPI spec. The decision lives in the application use case, resolved from canonical workspace state, not in the route: billing authority is payer identity and organization role, which the workspace permission ladder cannot express — a plain workspace `admin` is deliberately not enough.
The first pass gated `credits` and `storage` on billing authority for personal API keys but let a `workspace_api_key` principal through unconditionally, which left the excluded role a way back in. Any workspace `admin` may mint a workspace API key, and a workspace `admin` is deliberately not a billing manager, so an admin who reads `null` as themselves could mint a key and read the full pool with it. On an organization-hosted workspace that pool is the organization's, spanning workspaces the admin has no standing in. Billing authority is payer identity or an organization admin role — a property of a person. A workspace API key is deliberately actor-less, so it can never satisfy it and now reads both fields as `null`. Attributing the key to its creator was rejected: it would launder the same workspace-admin role, it breaks when the creator's authority is revoked while the key lives on, and substituting a key's owner for the acting principal is what the application operation boundary forbids. The reasoning sits in TSDoc at the decision point. The key keeps the plan, period, and standing it needs to monitor a workspace, including `limit_exceeded` and `billing_blocked`. No in-repo caller reads `credits` or `storage` from this endpoint. The payer storage pool is now read only once disclosure is authorized, so a caller who may not see it no longer triggers the query at all.
9667823 to
79a3486
Compare
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 79a3486. Configure here.
1667d3c
into
improvement/v2-route-standardization
Stacked on #6565 → #6560. Review only this PR's own commits; merge after its parents.
GET /api/v2/billing/statusreturned the resolved payer's pooledcredits(used/limit/remaining) andstorageto any caller holding onlyreadon the workspace.On an org-hosted workspace
resolveStorageBillingContextresolves the payer entity, sostorageis organization-wide across every workspace that org funds — not workspace-scoped.Baseline note
v2 billing does not exist on
origin/main, so "main gated this route" is literally false. But main's analogous surface deliberately gated the same data:workspaces/[id]/credit-availabilitypassescanViewPayerPool: canManageWorkspaceBilling(...)intogetWorkspaceCreditAvailability, which substitutes a member-scoped or null figure otherwise. No route on main ever returned a payer's pool to a non-billing-manager.plan,period,status, andworkspaceIdwere not newly exposed —workspaces/[id]/host-contextalready surfaces the payer's rolled-up plan to every workspace viewer. Those stay.Why concealment rather than a role gate
Billing authority is not a workspace role: it is
billedAccountUserId === userId, or org admin.defineWorkspaceOperationcaps workspace API keys atwrite, and a workspaceadminis deliberately not a billing manager, so nominimumRoleexpresses the policy. The endpoint stays atread; the use case projects the two pool objects only to an authorized caller. The route is untouched — no role check moved into the adapter.Workspace API keys are excluded
An actor-less
workspace_api_keyhas no user to evaluate, andPOST /api/workspaces/[id]/api-keysrequires only workspaceadmin— a freely grantable collaborator permission. Treating such keys as billing managers would let a non-billing-manager admin mint a key and read the org-wide pool. Attributing the key to its creator is ruled out by CLAUDE.md: "Never substitute a billing owner, uploader, creator, or API-key owner for the acting principal" — and is independently unsound, since the creator's authority can be revoked while the key keeps working.Keys keep
plan,period,status,workspaceId, which coverslimit_exceeded/billing_blockedalerting. No in-repo consumer readscredits/storage(checkedpackages/ts-sdk, all ofapps, docs).Also
The org-wide storage query now runs only after disclosure is authorized; previously it executed for every caller and the result was discarded.
creditsandstorageare now.nullable()in the contract with descriptions stating why; OpenAPI regenerated. One existing test asserted the old leak and was corrected.Reverting the guard turns 6 tests red across both the human and workspace-key paths. type-check · biome · 553 tests ·
check:api-validation·check:openapi— all pass.