Skip to content

fix(billing): withhold the payer credit and storage pools from callers who cannot manage billing - #6567

Merged
waleedlatif1 merged 3 commits into
improvement/v2-route-standardizationfrom
fix/v2-billing-status-authz
Aug 11, 2026
Merged

fix(billing): withhold the payer credit and storage pools from callers who cannot manage billing#6567
waleedlatif1 merged 3 commits into
improvement/v2-route-standardizationfrom
fix/v2-billing-status-authz

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Stacked on #6565#6560. Review only this PR's own commits; merge after its parents.

GET /api/v2/billing/status returned the resolved payer's pooled credits (used/limit/remaining) and storage to any caller holding only read on the workspace.

On an org-hosted workspace resolveStorageBillingContext resolves the payer entity, so storage is 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-availability passes canViewPayerPool: canManageWorkspaceBilling(...) into getWorkspaceCreditAvailability, 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, and workspaceId were not newly exposed — workspaces/[id]/host-context already 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. defineWorkspaceOperation caps workspace API keys at write, and a workspace admin is deliberately not a billing manager, so no minimumRole expresses the policy. The endpoint stays at read; 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_key has no user to evaluate, and POST /api/workspaces/[id]/api-keys requires only workspace admin — 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 covers limit_exceeded / billing_blocked alerting. No in-repo consumer reads credits/storage (checked packages/ts-sdk, all of apps, 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.

credits and storage are 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.

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview Aug 11, 2026 11:31pm

Request Review

@cursor

cursor Bot commented Aug 11, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes authorization for billing pool disclosure — previously leaked organization-wide credit and storage allowances to any workspace reader or API key. Correctness of the new gate is security-critical.

Overview
Closes a billing data leak on GET /api/v2/billing/status: credits and storage now return only for callers who can manage the resolved payer's billing. Everyone else — including all workspace API keys — gets null, while still receiving plan, period, and status for limit monitoring.

Adds server-side canUserManageWorkspaceBilling (org admin/owner, or billed account holder on personal workspaces). Workspace roles alone are not enough. Storage pool queries now run only after disclosure is authorized.

Makes credits and storage nullable in the v2 contract and OpenAPI docs.

Reviewed by Cursor Bugbot for commit 79a3486. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR prevents callers without billing-management authority from receiving payer-level pooled credit and storage data while preserving workspace billing standing.

  • Adds a canonical server-side billing-authority predicate for personal and organization-hosted workspaces.
  • Returns nullable credits and storage fields and avoids resolving storage usage when disclosure is unauthorized.
  • Updates tests, schemas, generated OpenAPI documentation, and generator assertions for the revised contract.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

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]
Loading

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.
@waleedlatif1
waleedlatif1 force-pushed the fix/v2-billing-status-authz branch from 9667823 to 79a3486 Compare August 11, 2026 23:24
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile-apps

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor 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.

✅ 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.

@waleedlatif1
waleedlatif1 changed the base branch from fix/archive-extraction-folder-path to improvement/v2-route-standardization August 11, 2026 23:34
@waleedlatif1
waleedlatif1 merged commit 1667d3c into improvement/v2-route-standardization Aug 11, 2026
5 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/v2-billing-status-authz branch August 12, 2026 00:18
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