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(api): pin two authorization invariants, and say what these reads are scoped to
Nothing asserted that a secret list is narrowed to its caller's own rows.
The query is mocked, so its canned result proved nothing about what was
asked for, and removing the narrowing left the suite green. The same was
true of the refusal that keeps a workspace key — which acts for a workspace,
not a person — out of a personal secret list: with the policy flipped the
use case resolved and returned secrets. Both are pinned by the arguments
the query is built from.
Personal secrets remain invisible from a workspace the caller reaches
through inherited organization access. Their metadata has no canonical row,
only per-workspace mirrors written for a narrower set of workspaces than a
caller can authorize into, so a list scoped to one workspace cannot find
them while set and delete, which key on the person, work. Dropping the
scope would return one row per mirror and break pagination, and a mirror's
timestamps describe when it was written rather than when the secret was
made, so the sort has no meaning. Closing it is a storage change; the
divergence is recorded where the read happens and stated in the contract
rather than left implied.
Account-wide billing is requested by omitting the workspace, which is
indistinguishable from an ordinary request, so a workspace key asking for
it is answered with its own workspace rather than refused. The refusal
belongs where the intent exists, which is the client; the contract now says
what omission means for each kind of key, and that a workspace-scoped page
reports its members in aggregate and names none of them.
A run is addressed by an id that already names its workspace, so that read
takes no workspace and refuses one that is sent. Unlike its siblings,
nothing said so.
Copy file name to clipboardExpand all lines: apps/sim/lib/api/contracts/v2/billing.ts
+5-4Lines changed: 5 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -42,7 +42,7 @@ export const v2BillingStatusQuerySchema = z
42
42
workspaceId: workspaceIdSchema
43
43
.optional()
44
44
.describe(
45
-
'Workspace whose payer should be resolved. A workspace API key is pinned to its own workspace: any other id answers `404 Workspace not found`, which is also what an id that does not exist answers.'
45
+
'Workspace whose payer should be resolved. Omitting it selects account scope — the payer behind the calling *account*, across every workspace — and only a personal API key has an account to select. A workspace API key that omits it still resolves its own workspace, because omission is the same request as sending that key its own id; it is not a way to widen a workspace key. The response `workspaceId` reports which was resolved and is `null` only on account scope. A workspace API key is pinned to its own workspace: any other id answers `404 Workspace not found`, which is also what an id that does not exist answers.'
46
46
),
47
47
})
48
48
.strict()
@@ -172,7 +172,7 @@ export const v2BillingLogsQuerySchema = z
172
172
workspaceId: workspaceIdSchema
173
173
.optional()
174
174
.describe(
175
-
"Narrow the ledger to usage events attributed to one workspace. It does not change whose events are reported — a personal API key always reports the usage of the person holding it, and a workspace API key always reports its own workspace's complete ledger across every member. The response `scope` field says which of the two you received. A workspace API key is pinned to its own workspace: any other id answers `404 Workspace not found`, which is also what an id that does not exist answers."
175
+
"Narrow the ledger to usage events attributed to one workspace. It does not change whose events are reported — a personal API key always reports the usage of the person holding it, and a workspace API key always reports its own workspace's complete ledger across every member. The response `scope` field says which of the two you received. Omitting it does not widen a workspace API key: that key has no account behind it, so an omitted id is the same request as its own id and the page still covers exactly one workspace, reported as `scope: workspace`. A ledger spanning every workspace an account touches requires a personal API key. A workspace API key is pinned to its own workspace: any other id answers `404 Workspace not found`, which is also what an id that does not exist answers."
176
176
),
177
177
period: usageLogPeriodSchema
178
178
.optional()
@@ -278,7 +278,8 @@ export const v2BillingLogEntrySchema = z
278
278
.meta({
279
279
id: 'V2BillingLogEntry',
280
280
title: 'Billing log entry',
281
-
description: 'One credit-consuming usage event in the billing ledger.',
281
+
description:
282
+
'One credit-consuming usage event in the billing ledger. No field identifies the member who incurred it: on `workspace` scope a page is every member’s usage in aggregate, not a per-member breakdown, so it reconciles a workspace’s spend without publishing who spent it.',
@@ -292,7 +293,7 @@ export type V2BillingLogEntry = z.output<typeof v2BillingLogEntrySchema>
292
293
exportconstv2BillingLogsScopeSchema=z
293
294
.enum(['user','workspace'])
294
295
.describe(
295
-
"Whose usage this page reports. `user` — the events of the person whose personal API key made the request, narrowed by `workspaceId` when one was given; this omits other members' usage. `workspace` — every member's events for the workspace a workspace API key is pinned to."
296
+
"Whose usage this page reports. `user` — the events of the person whose personal API key made the request, narrowed by `workspaceId` when one was given; this omits other members' usage. `workspace` — every member's events for the workspace a workspace API key is pinned to, in aggregate: no entry names the member it belongs to."
runId: runIdSchema.describe('Unique workflow run identifier.'),
310
+
runId: runIdSchema.describe(
311
+
'Unique workflow run identifier. A run is addressed globally by this id: unlike the list and statistics routes, this route takes no workspace. The run carries its own workspace and the caller is authorized against that one, so a run the caller cannot reach is concealed as a 404 rather than filtered out.'
Copy file name to clipboardExpand all lines: apps/sim/lib/api/contracts/v2/secrets.ts
+5-1Lines changed: 5 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -96,7 +96,11 @@ export type V2SecretSortBy = (typeof v2SecretSortFields)[number]
96
96
exportconstv2ListSecretsQuerySchema=z
97
97
.object({
98
98
workspaceId: workspaceIdSchema.describe('Workspace whose secret metadata should be listed.'),
99
-
scope: v2SecretScopeSchema.optional().describe('Restrict results to one ownership scope.'),
99
+
scope: v2SecretScopeSchema
100
+
.optional()
101
+
.describe(
102
+
"Restrict results to one ownership scope. Personal results are not the caller's full personal set: this list reads the per-workspace credential mirrors of a personal secret, and a mirror exists only for workspaces the caller holds an explicit membership or ownership of. A personal secret is therefore omitted here when the caller reaches this workspace through inherited organization access, even though the same secret can be set and deleted from it. Prefer listing from a workspace the caller is an explicit member of until the mirrors are replaced by canonical personal-secret metadata."
103
+
),
100
104
search: v2SearchSchema.describe('Case-insensitive substring match against the secret name.'),
0 commit comments