Skip to content

Commit 60b672e

Browse files
os-helpclaude[bot]claude
authored
fix(spec,platform-objects): register sys_api_key's deliberate no-batch decision (#7802) (#7818)
The `apiMethods` conformance scan in `@objectstack/spec` went red on `main` after #7769 gave `sys_api_key` `update` without `bulk`. Resolved as the rule's second documented outcome — a `SINGLE_RECORD_WRITE_ONLY` exemption carrying the evidence — rather than by widening the object's API surface. Evidence: the console renders no checkbox column for this object (multi-select is auto-enabled only when a bulk action exists; the only implicit one is bulk-delete, and the object grants no delete affordance), and a promoted multi-select revoke would fan out per row through the action runner rather than reaching `/batch`. #7769's `update` grant is untouched. Claude-Session: https://claude.ai/code/session_0158ZQo7LiHSxGWpYKuPq1wu Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 098b629 commit 60b672e

3 files changed

Lines changed: 85 additions & 3 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/platform-objects": patch
4+
---
5+
6+
fix(spec,platform-objects): put `sys_api_key`'s missing batch route on the record (#7802)
7+
8+
`@objectstack/spec`'s `apiMethods` conformance scan was failing on `main` — and,
9+
because the scan lives in `spec` while the object it judges lives in
10+
`platform-objects`, failing for every PR that touched `spec` and no others.
11+
#7769 had added `update` to `sys_api_key`'s `enable.apiMethods` so the Setup
12+
UI's Revoke button had a working route, which tripped the rule "a whitelist that
13+
grants single-record writes must also grant `bulk`".
14+
15+
Resolved as the rule's second documented outcome — a registered exemption, not a
16+
widened object. `sys_api_key` now carries the monorepo's only
17+
`SINGLE_RECORD_WRITE_ONLY` entry, with the evidence behind it:
18+
19+
- **No batch surface exists to deny.** The console renders no checkbox column on
20+
any of the object's list views: multi-select is auto-enabled only when a bulk
21+
action exists, the sole implicit one is bulk-delete, and this object grants no
22+
delete affordance (`managedBy: 'better-auth'` denies by default, `userActions`
23+
opens `edit` alone, `delete` is not in `apiMethods`).
24+
- **A future multi-select revoke would not need `bulk` either.** `revoke_api_key`
25+
/ `restore_api_key` are `list_item` actions; promoting one into a view's
26+
`bulkActions` resolves it to a `custom` def that the grid executor fans out
27+
through the action runner as N single-record PATCHes — never `/batch`.
28+
29+
So `POST /api/v1/data/sys_api_key/batch` and the `*Many` routes keep answering
30+
405 for API keys, deliberately: the object's authorable surface is the single
31+
`revoked` boolean that ADR-0092 D2's identity write guard admits, and nothing
32+
asks to write it in bulk. #7769's `update` grant is untouched — the Revoke
33+
button keeps working. Adding `bulk` later requires retiring the exemption in the
34+
same commit; the conformance suite's stale-entry check refuses to let both stand.

packages/platform-objects/src/identity/sys-api-key.object.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,19 @@ export const SysApiKey = ObjectSchema.create({
259259
// `key`, `user_id`, `expires_at`, `name`, … — is stripped, and a PATCH
260260
// that touches nothing else is refused 403 `PERMISSION_DENIED` rather
261261
// than degrading into a silent no-op.
262+
//
263+
// The batch primitive stays off DELIBERATELY (#7802), which is why this is
264+
// the monorepo's only single-record-write whitelist without it. Revoking is
265+
// a one-row, one-column gesture: the console renders no checkbox column for
266+
// this object (no bulk action can arise — the grid's only implicit one is
267+
// bulk-delete, and this object grants no delete affordance), and promoting
268+
// a row action into a view's `bulkActions` fans out per row through the
269+
// action runner rather than calling `/batch`. So the primitive would open
270+
// `POST /data/sys_api_key/batch` and the `*Many` routes to API clients for
271+
// no caller. The decision is on the record — with its evidence and the
272+
// conditions that would reverse it — in `SINGLE_RECORD_WRITE_ONLY` in
273+
// `@objectstack/spec`'s `data/api-methods-batch-conformance.test.ts`, whose
274+
// stale-entry check fails if `bulk` is added here without retiring it.
262275
apiMethods: ['get', 'list', 'update'],
263276
},
264277
});

packages/spec/src/data/api-methods-batch-conformance.test.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,49 @@ const WRITE_PRIMITIVES = ['create', 'update', 'delete'] as const;
4242

4343
/**
4444
* Objects that deliberately expose single-record writes but NO batch route,
45-
* keyed by object name with the reason. Empty today: every tightened whitelist
46-
* in the monorepo either grants `bulk` or grants no write verb at all.
45+
* keyed by object name with the reason. Every other tightened whitelist in the
46+
* monorepo either grants `bulk` or grants no write verb at all.
4747
*
4848
* Adding an entry is a real decision — batch denial is invisible until a user
4949
* multi-selects rows and `data-objectstack` rethrows the 405 without falling
5050
* back to per-row writes. Write down why the object is worth that.
5151
*/
52-
const SINGLE_RECORD_WRITE_ONLY: Record<string, string> = {};
52+
const SINGLE_RECORD_WRITE_ONLY: Record<string, string> = {
53+
// #7802. `update` arrived in #7727/#7769 for exactly one gesture on exactly
54+
// one column: the `revoke_api_key` / `restore_api_key` row actions PATCH
55+
// `revoked` on ONE key. The multi-select surface this rule protects does not
56+
// exist for API keys, and the shape a future one would take does not need
57+
// `bulk` either — both read off the console build this release pins
58+
// (`.objectui-sha` 6314e87f2, `packages/plugin-grid`):
59+
//
60+
// · No checkbox column is rendered. None of the object's four list views
61+
// declares `bulkActions` / `bulkActionDefs` / `selection`, and `ObjectGrid`
62+
// auto-enables multi-select only when a bulk action exists. The single
63+
// implicit one is bulk-delete, gated on the resolved `delete` affordance —
64+
// false here three times over (`managedBy: 'better-auth'` denies by
65+
// default, `userActions` opens `edit` alone, and `delete` is not in
66+
// `apiMethods`). So there is no selection to batch.
67+
// · A multi-select revoke, if the product ever wants one, still would not
68+
// reach `/batch`. Both actions are `locations: ['list_item']`; naming one
69+
// in a view's `bulkActions` promotes it to `operation: 'custom'` +
70+
// `actionDef`, which `useBulkExecutor` fans out through the action runner
71+
// as N single-record PATCHes against the route #7769 opened. The data-plane
72+
// `bulk` primitive is reached only by an `update`/`delete` bulk def, which
73+
// this object neither declares nor can acquire implicitly.
74+
//
75+
// Granting `bulk` would therefore open `POST /data/sys_api_key/batch` and the
76+
// `*Many` routes to every API client, on a better-auth identity table whose
77+
// authorable surface is one boolean — ADR-0092 D2's write guard whitelists
78+
// `revoked` and strips everything else — with no consumer asking for it.
79+
// Should a batch key lifecycle ever gain a real caller, delete this entry and
80+
// add `'bulk'`; the stale-entry test below refuses to let both stand.
81+
sys_api_key:
82+
'Revoke/restore is a one-row, one-column PATCH (`revoked`, the only column ' +
83+
"ADR-0092 D2's identity write guard admits). No console surface multi-selects " +
84+
'API keys — the grid renders no checkbox column because the object grants no ' +
85+
'delete affordance — and a promoted bulk revoke would fan out per row through ' +
86+
'the action runner rather than hitting /batch (#7802).',
87+
};
5388

5489
/** Every `*.object.ts` under `packages/`, skipping build output and deps. */
5590
function walkObjectFiles(dir: string, out: string[] = []): string[] {

0 commit comments

Comments
 (0)