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
{{ message }}
Repository navigation
Commit 32f4c5c
Browse filesBrowse the repository at this point in the historyBrowse files
fix(billing): deny the payer pool to actor-less workspace API keys
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.
Copy file name to clipboardExpand all lines: apps/docs/openapi-v2-billing.json
+3-3Lines changed: 3 additions & 3 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. `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.",
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; they are always null for a workspace API key. 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
{
@@ -567,7 +567,7 @@
567
567
"type": "null"
568
568
}
569
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."
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. Always null for a workspace API key."
571
571
},
572
572
"storage": {
573
573
"anyOf": [
@@ -597,7 +597,7 @@
597
597
"type": "null"
598
598
}
599
599
],
600
-
"description": "The payer's storage consumption and quota, or null when the caller cannot manage that payer's billing."
600
+
"description": "The payer's storage consumption and quota, or null when the caller cannot manage that payer's billing. Always null for a workspace API key."
* `credits` and `storage` report the resolved payer's pooled allowances, which
40
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.
41
+
* for a caller who may manage that payer's billing: the billed account holder,
42
+
* or an admin of the hosting organization. Billing authority is a property of
43
+
* a person, so an actor-less workspace API key never qualifies. Every other
44
+
* caller reads both as `null` while still seeing the plan, period, and
45
+
* standing that the workspace already surfaces to them — enough to monitor for
46
+
* `limit_exceeded` and `billing_blocked`.
46
47
*/
47
48
exportconstv2BillingStatusDataSchema=z
48
49
.object({
@@ -88,7 +89,7 @@ export const v2BillingStatusDataSchema = z
88
89
})
89
90
.nullable()
90
91
.describe(
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."
92
+
"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. Always null for a workspace API key."
92
93
),
93
94
storage: z
94
95
.object({
@@ -98,7 +99,7 @@ export const v2BillingStatusDataSchema = z
98
99
})
99
100
.nullable()
100
101
.describe(
101
-
"The payer's storage consumption and quota, or null when the caller cannot manage that payer's billing."
102
+
"The payer's storage consumption and quota, or null when the caller cannot manage that payer's billing. Always null for a workspace API key."
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. `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.",
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; they are always null for a workspace API key. 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