Skip to content

Commit b3a63d3

Browse files
os-samclaude
andauthored
feat(spec): retire the ten inert RestServerConfig keys the liveness ledger recorded as dead (ADR-0049 enforce-or-remove) (#14868)
* feat(spec): retire the ten inert RestServerConfig keys (WIP checkpoint) Tombstone crud.patterns/objectParamStyle, metadata.cacheTtl/endpoints.schema, batch.operations.upsertMany/defaultAtomic and all of routes.* under ADR-0049 enforce-or-remove (#14369 census); drop the orphaned CrudEndpointPatternSchema; keep the ledger rows dead with REMOVED notes; reverse the #11984 pins. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5bnauQVfR8NbAm4QagXzj * test(spec,rest): reverse the #11984 pins into refusal pins; regenerate artifacts Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5bnauQVfR8NbAm4QagXzj * fix(spec): keep issue ids out of the tombstone prescriptions; re-anchor census docs; re-record test-typecheck debt Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5bnauQVfR8NbAm4QagXzj * chore: regenerate the system-context census page after merging main Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5bnauQVfR8NbAm4QagXzj * chore: regenerate the system-context census page after merging main Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5bnauQVfR8NbAm4QagXzj --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a8a674a commit b3a63d3

37 files changed

Lines changed: 1135 additions & 501 deletions
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/rest": minor
4+
---
5+
6+
feat(spec): retire the ten inert `RestServerConfig` keys the liveness ledger recorded as `dead` (#14691, ADR-0049 enforce-or-remove)
7+
8+
<!-- adr-0087: registered rest-server-config-dead-keys-retired -->
9+
10+
**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
11+
launch-window convention ships it as `minor`; the prescription is registered
12+
under protocol major 18, where `os migrate meta` users will look).
13+
14+
#14369 enrolled the four `RestServerConfig` sub-objects (`crud`, `metadata`,
15+
`batch`, `routes`) in the spec liveness ledger and found 15 of their 32 rows
16+
`dead`: parsed, defaulted and normalized into the REST server's config by
17+
`normalizeConfig` (#11984) and never read back. This change is the
18+
enforce-or-remove call on every one of them, taken per family, and every
19+
family resolved to REMOVE — each promised capability either already exists at
20+
its proper seat or would contradict a fixed contract. The closed-set cloud
21+
sweep (#14796, `objectstack-ai/cloud` @ `9b6abe0f2fd5`) returned zero hits,
22+
structurally: cloud never authors a `RestServerConfig`.
23+
24+
**What is refused:** authoring any of the ten keys below. Each is a
25+
`retiredKey()` tombstone (all four sub-schemas are non-strict `z.object()`s,
26+
so a plain deletion would have silently stripped the key), so authoring it is
27+
a `tsc` error and a parse error carrying the prescription — and, because
28+
`RestServer` parses these sub-objects at construction (#11984),
29+
`new RestServer(...)` / `createRestApiPlugin().start()` now refuse a config
30+
that carries one, naming the sub-object, the key and the declaring schema.
31+
32+
**FROM → TO** (delete the key in every case; none ever had an effect to preserve):
33+
34+
- `crud.patterns` → the mounted CRUD paths are the contract the client SDK, the
35+
discovery document and the served `/openapi.json` all describe; `crud.dataPrefix`
36+
moves them deployment-wide. An endpoint on a custom path or method is a
37+
declarative `api` endpoint (`type: 'object_operation'`). Its value schema
38+
`CrudEndpointPatternSchema` / type `CrudEndpointPattern` are removed with it
39+
(no other consumer; `CrudOperation` stays — `GeneratedEndpoint.operation` reads it).
40+
- `crud.objectParamStyle` → the object name is always a path segment.
41+
- `metadata.cacheTtl``metadata.enableCache` is the live switch (it selects the
42+
protocol's `getMetaItemCached` path, which takes no TTL); a declarative `api`
43+
endpoint's `cacheTtl` is the key that reaches the wire. The unbounded negative
44+
TTL this key accepted goes with it.
45+
- `metadata.endpoints.schema` → gated `GET /meta/:type/:name/schema`, which does
46+
not exist; `endpoints.types` / `items` / `item` gate real mounts and stay.
47+
- `batch.operations.upsertMany` → gated `POST /data/:object/upsertMany`, which
48+
was never built; upsert is an operation type of the generic
49+
`POST /data/:object/batch` endpoint (`BatchOperationType` `'upsert'`), gated
50+
by `batch.enableBatchEndpoint`.
51+
- `batch.defaultAtomic` → atomicity is the per-request `options.atomic`
52+
(ADR-0119 D4, opt-in); a server-side default that flipped it silently would
53+
change the failure semantics of callers who send nothing, which that ADR
54+
refused. Callers that need all-or-nothing send `options: { atomic: true }`.
55+
- `routes.includeObjects` / `routes.excludeObjects` / `routes.overrides`
56+
(`enabled` / `basePath` / `operations`) → per-object API exposure is declared
57+
ON the object and enforced by the REST data surface: `enable.apiEnabled: false`
58+
hides it (404), `enable.apiMethods` whitelists its operations (405). The data
59+
base path is deployment-wide (`crud.dataPrefix`).
60+
- `routes.nameTransform` → the object `name` is the canonical id on every
61+
surface, the REST path segment included; there is no URL transform to configure.
62+
63+
**What stays, byte-identical:** every live key of the four sub-objects —
64+
`crud.operations.*`, `crud.dataPrefix`, `metadata.prefix` / `enableCache` /
65+
`maskObjectFields` / `endpoints.types|items|item`, `batch.maxBatchSize` /
66+
`enableBatchEndpoint` / `operations.createMany|updateMany|deleteMany` — with
67+
its default and its mount. The mounted REST surface does not change: none of
68+
the ten keys ever reached it. `@objectstack/rest`'s normalized config no longer
69+
carries the retired keys (they were written and never read), and the #11984
70+
pins of their accept/reject behaviour are reversed to refusal pins, by design.
71+
72+
The retirement kit: `retiredKey()` tombstones on the four sub-schemas;
73+
`RestServerConfigSchema`'s `@example` no longer advertises
74+
`routes: { excludeObjects: [...] }`; ledger rows kept `dead` with a REMOVED
75+
note and `evidenceScope: cross-repo` (the two container rows collapse into one
76+
each, since their child keys left the walked shape); `RETIRED_KEYS_BY_MAJOR[18]`
77+
× 10 and `RETIRED_DEFS_BY_MAJOR[18]` `api/CrudEndpointPattern`; D3 semantic
78+
entry `rest-server-config-dead-keys-retired`. No D2 conversion: a
79+
`RestServerConfig` is plugin TS configuration (REST plugin constructor /
80+
`plugin-hono-server` `restConfig`), never a stack collection member or a
81+
`sys_metadata` row (the `openApi31` precedent, #4579), so there is no source
82+
for the chain to rewrite and the prescription carries no `os migrate meta`
83+
sentence.

content/docs/permissions/system-context.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ not on any flag.
6464
## How the flag is set
6565

6666
`isSystem` is **server-constructed and never client-supplied**. Inbound HTTP
67-
cannot set it (`packages/rest/src/rest-server.ts:1522`, `:1551`), and neither
67+
cannot set it (`packages/rest/src/rest-server.ts:1520`, `:1549`), and neither
6868
can an action body (`packages/runtime/src/domains/actions.ts:404`). It is
6969
written by internal callers only, as an option on the engine call:
7070

@@ -103,7 +103,7 @@ that silently does not happen.
103103
| 14 | MCP stdio bridge skips the object API-exposure gate | mcp | Get: the bridge reaches objects whose `apiEnabled` / `apiMethods` would refuse an external caller | `stdio-data-bridge.ts:246` |
104104
| 15 | **Read-audit rows are not written** | plugin-audit | Lose: the "a person opened this record" trail. `sudo()` keeps the caller's `userId`, so this flag is the only thing separating a human read from a platform one | `read-audit.ts:556` |
105105
| 16 | Approval snapshot payload redaction skipped | plugin-approvals | Get: the whole snapshot on `find` / `findOne` — the audit/replay channel. Lose: field-visibility redaction over approval payloads | `payload-redaction-middleware.ts:115` |
106-
| 17 | REST anonymous-deny seam satisfied | rest | Get: `enforceAuth` passes with no `userId`. Not reachable from the wire — `isSystem` is never set on an inbound request | `rest-server.ts:1554` |
106+
| 17 | REST anonymous-deny seam satisfied | rest | Get: `enforceAuth` passes with no `userId`. Not reachable from the wire — `isSystem` is never set on an inbound request | `rest-server.ts:1552` |
107107

108108
### 2. Write pipeline and data integrity
109109

@@ -199,7 +199,7 @@ assuming `isSystem` covers it is a documented source of bugs.
199199
| "It preserves a supplied `updated_at` / `updated_by`" | **No.** That is `preserveAudit`, a separate opt-in — and an UPDATE-path exemption only | `field.zod.ts:1516` (#3493 / #6640) |
200200
| "It stamps `created_by`" | **No.** Audit stamping reads `userId` from the context. A user-less system write stamps nothing — that is today's behaviour, not an error | `runtime-identity.ts:280``281` |
201201
| "It bypasses every guard" | **No.** The last-admin guard applies to **every** context, `isSystem` included — the deprovision path that actually locks an org out is the system one | `last-admin-guard.ts:299` |
202-
| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1522`, `:1551`; `domains/actions.ts:404` |
202+
| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1520`, `:1549`; `domains/actions.ts:404` |
203203

204204
---
205205

0 commit comments

Comments
 (0)