Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/sys-api-key-no-batch-route.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
"@objectstack/spec": patch
"@objectstack/platform-objects": patch
---

fix(spec,platform-objects): put `sys_api_key`'s missing batch route on the record (#7802)

`@objectstack/spec`'s `apiMethods` conformance scan was failing on `main` — and,
because the scan lives in `spec` while the object it judges lives in
`platform-objects`, failing for every PR that touched `spec` and no others.
#7769 had added `update` to `sys_api_key`'s `enable.apiMethods` so the Setup
UI's Revoke button had a working route, which tripped the rule "a whitelist that
grants single-record writes must also grant `bulk`".

Resolved as the rule's second documented outcome — a registered exemption, not a
widened object. `sys_api_key` now carries the monorepo's only
`SINGLE_RECORD_WRITE_ONLY` entry, with the evidence behind it:

- **No batch surface exists to deny.** The console renders no checkbox column on
any of the object's list views: multi-select is auto-enabled only when a bulk
action exists, the sole implicit one is bulk-delete, and this object grants no
delete affordance (`managedBy: 'better-auth'` denies by default, `userActions`
opens `edit` alone, `delete` is not in `apiMethods`).
- **A future multi-select revoke would not need `bulk` either.** `revoke_api_key`
/ `restore_api_key` are `list_item` actions; promoting one into a view's
`bulkActions` resolves it to a `custom` def that the grid executor fans out
through the action runner as N single-record PATCHes — never `/batch`.

So `POST /api/v1/data/sys_api_key/batch` and the `*Many` routes keep answering
405 for API keys, deliberately: the object's authorable surface is the single
`revoked` boolean that ADR-0092 D2's identity write guard admits, and nothing
asks to write it in bulk. #7769's `update` grant is untouched — the Revoke
button keeps working. Adding `bulk` later requires retiring the exemption in the
same commit; the conformance suite's stale-entry check refuses to let both stand.
13 changes: 13 additions & 0 deletions packages/platform-objects/src/identity/sys-api-key.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,19 @@ export const SysApiKey = ObjectSchema.create({
// `key`, `user_id`, `expires_at`, `name`, … — is stripped, and a PATCH
// that touches nothing else is refused 403 `PERMISSION_DENIED` rather
// than degrading into a silent no-op.
//
// The batch primitive stays off DELIBERATELY (#7802), which is why this is
// the monorepo's only single-record-write whitelist without it. Revoking is
// a one-row, one-column gesture: the console renders no checkbox column for
// this object (no bulk action can arise — the grid's only implicit one is
// bulk-delete, and this object grants no delete affordance), and promoting
// a row action into a view's `bulkActions` fans out per row through the
// action runner rather than calling `/batch`. So the primitive would open
// `POST /data/sys_api_key/batch` and the `*Many` routes to API clients for
// no caller. The decision is on the record — with its evidence and the
// conditions that would reverse it — in `SINGLE_RECORD_WRITE_ONLY` in
// `@objectstack/spec`'s `data/api-methods-batch-conformance.test.ts`, whose
// stale-entry check fails if `bulk` is added here without retiring it.
apiMethods: ['get', 'list', 'update'],
},
});
41 changes: 38 additions & 3 deletions packages/spec/src/data/api-methods-batch-conformance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,49 @@ const WRITE_PRIMITIVES = ['create', 'update', 'delete'] as const;

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

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