You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(billing): withhold the payer credit pool from v2 status readers
`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.
Copy file name to clipboardExpand all lines: apps/docs/openapi-v2-billing.json
+50-36Lines changed: 50 additions & 36 deletions
Original file line number
Diff line number
Diff line change
@@ -36,7 +36,7 @@
36
36
"get": {
37
37
"operationId": "getBillingStatus",
38
38
"summary": "Get Billing Status",
39
-
"description": "Return the current plan, billing standing, credit allowance, and storage quota. Billing history lives at `GET /api/v2/billing/logs`. Without a Stripe subscription — notably on the free plan — there is no real billing period: `period` is the open interval 1970-01-01 to 9999-12-31 and `credits.used` is lifetime consumption, not consumption since a period start.",
39
+
"description": "Return the current plan, billing standing, credit allowance, and storage quota. `credits` and `storage` report the payer's pooled allowances and are null unless the caller can manage that payer's billing. Billing history lives at `GET /api/v2/billing/logs`. Without a Stripe subscription — notably on the free plan — there is no real billing period: `period` is the open interval 1970-01-01 to 9999-12-31 and `credits.used` is lifetime consumption, not consumption since a period start.",
40
40
"tags": ["Billing"],
41
41
"parameters": [
42
42
{
@@ -543,47 +543,61 @@
543
543
"description": "Current billing standing."
544
544
},
545
545
"credits": {
546
-
"type": "object",
547
-
"properties": {
548
-
"used": {
549
-
"type": "number",
550
-
"description": "Credits consumed so far. The counter is reset by Stripe invoice webhooks, so on a paid plan it covers the current billing period; on the free plan nothing resets it and the value is lifetime consumption."
551
-
},
552
-
"limit": {
553
-
"type": "number",
554
-
"description": "Credit allowance for the reporting window — per billing period on a paid plan, lifetime on the free plan."
546
+
"anyOf": [
547
+
{
548
+
"type": "object",
549
+
"properties": {
550
+
"used": {
551
+
"type": "number",
552
+
"description": "Credits consumed so far. The counter is reset by Stripe invoice webhooks, so on a paid plan it covers the current billing period; on the free plan nothing resets it and the value is lifetime consumption."
553
+
},
554
+
"limit": {
555
+
"type": "number",
556
+
"description": "Credit allowance for the reporting window — per billing period on a paid plan, lifetime on the free plan."
557
+
},
558
+
"remaining": {
559
+
"type": "number",
560
+
"description": "Allowance minus consumption, over the same window."
561
+
}
562
+
},
563
+
"required": ["used", "limit", "remaining"],
564
+
"additionalProperties": false
555
565
},
556
-
"remaining": {
557
-
"type": "number",
558
-
"description": "Allowance minus consumption, over the same window."
566
+
{
567
+
"type": "null"
559
568
}
560
-
},
561
-
"required": ["used", "limit", "remaining"],
562
-
"additionalProperties": false,
563
-
"description": "Credit usage and allowance. Periodic on a paid plan; lifetime on the free plan, where the counter never resets."
569
+
],
570
+
"description": "The payer's credit usage and allowance — periodic on a paid plan, lifetime on the free plan, where the counter never resets. Null when the caller cannot manage that payer's billing."
564
571
},
565
572
"storage": {
566
-
"type": "object",
567
-
"properties": {
568
-
"usedBytes": {
569
-
"type": "number",
570
-
"minimum": 0,
571
-
"description": "Storage currently consumed, in bytes."
572
-
},
573
-
"limitBytes": {
574
-
"type": "number",
575
-
"minimum": 0,
576
-
"description": "Storage quota, in bytes."
573
+
"anyOf": [
574
+
{
575
+
"type": "object",
576
+
"properties": {
577
+
"usedBytes": {
578
+
"type": "number",
579
+
"minimum": 0,
580
+
"description": "Storage currently consumed, in bytes."
581
+
},
582
+
"limitBytes": {
583
+
"type": "number",
584
+
"minimum": 0,
585
+
"description": "Storage quota, in bytes."
586
+
},
587
+
"percentUsed": {
588
+
"type": "number",
589
+
"minimum": 0,
590
+
"description": "Percentage of the storage quota consumed."
* Current billing standing, credit allowance, and storage quota. Ledger rows
37
37
* and source analytics deliberately live outside this status resource.
38
+
*
39
+
* `credits` and `storage` report the resolved payer's pooled allowances, which
40
+
* are shared across every workspace that payer funds. They are populated only
41
+
* for a caller who may manage that payer's billing — the billed account
42
+
* holder, an admin of the hosting organization, or a workspace API key, which
43
+
* an admin of that workspace provisioned. Any other workspace member reads
44
+
* both as `null` while still seeing the plan, period, and standing that the
45
+
* workspace already surfaces to them.
38
46
*/
39
47
exportconstv2BillingStatusDataSchema=z
40
48
.object({
@@ -78,16 +86,20 @@ export const v2BillingStatusDataSchema = z
78
86
),
79
87
remaining: z.number().describe('Allowance minus consumption, over the same window.'),
80
88
})
89
+
.nullable()
81
90
.describe(
82
-
'Credit usage and allowance. Periodic on a paid plan; lifetime on the free plan, where the counter never resets.'
91
+
"The payer's credit usage and allowance — periodic on a paid plan, lifetime on the free plan, where the counter never resets. Null when the caller cannot manage that payer's billing."
83
92
),
84
93
storage: z
85
94
.object({
86
95
usedBytes: z.number().nonnegative().describe('Storage currently consumed, in bytes.'),
87
96
limitBytes: z.number().nonnegative().describe('Storage quota, in bytes.'),
88
97
percentUsed: z.number().nonnegative().describe('Percentage of the storage quota consumed.'),
89
98
})
90
-
.describe('Current storage consumption and quota.'),
99
+
.nullable()
100
+
.describe(
101
+
"The payer's storage consumption and quota, or null when the caller cannot manage that payer's billing."
Copy file name to clipboardExpand all lines: apps/sim/lib/api/contracts/v2/openapi/billing.ts
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -80,7 +80,7 @@ const routes = [
80
80
operationId: 'getBillingStatus',
81
81
summary: 'Get Billing Status',
82
82
description:
83
-
'Return the current plan, billing standing, credit allowance, and storage quota. Billing history lives at `GET /api/v2/billing/logs`. Without a Stripe subscription — notably on the free plan — there is no real billing period: `period` is the open interval 1970-01-01 to 9999-12-31 and `credits.used` is lifetime consumption, not consumption since a period start.',
83
+
"Return the current plan, billing standing, credit allowance, and storage quota. `credits` and `storage` report the payer's pooled allowances and are null unless the caller can manage that payer's billing. Billing history lives at `GET /api/v2/billing/logs`. Without a Stripe subscription — notably on the free plan — there is no real billing period: `period` is the open interval 1970-01-01 to 9999-12-31 and `credits.used` is lifetime consumption, not consumption since a period start.",
84
84
errors: [...WORKSPACE_ERRORS,'NotFound'],
85
85
success: {description: 'The current billing and storage status.'},
0 commit comments