|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +--- |
| 4 | + |
| 5 | +feat(spec): `api` is a declared metadata kind — `DEFAULT_METADATA_TYPE_REGISTRY` + `BUILTIN_METADATA_TYPE_SCHEMAS` (#5271, part of #5206) |
| 6 | + |
| 7 | +`api` items were produced, indexed and executed while the spec declared the kind |
| 8 | +nowhere. Artifact ingest maps `defineStack({ apis })` to `api` metadata |
| 9 | +(`ARTIFACT_FIELD_TO_TYPE`), the endpoint matcher indexes them |
| 10 | +(`buildEndpointIndex`), and #5040's executor serves them — but |
| 11 | +`DEFAULT_METADATA_TYPE_REGISTRY` had no `{ type: 'api', … }` entry and |
| 12 | +`BUILTIN_METADATA_TYPE_SCHEMAS` had no `api` binding. So |
| 13 | +`getMetadataTypeSchema('api')` returned `undefined` and `saveMetaItem` took its |
| 14 | +documented "unregistered type is stored without validation" branch: |
| 15 | +`PUT /api/v1/meta/api/:name` accepted **any JSON** and answered 200. That is |
| 16 | +`declared ≠ enforced` read backwards — enforced but **undeclared**. |
| 17 | + |
| 18 | +Both halves are now declared, which is one fix with two faces: |
| 19 | + |
| 20 | +- **A body is validated.** The existing 422 `invalid_metadata` path applies to |
| 21 | + `api` like every other kind, with structured Zod issues naming the offending |
| 22 | + key. An endpoint with no `target`, or no `type`, is refused instead of stored. |
| 23 | +- **The type is describable.** `/meta/types` emits a real JSON Schema and a |
| 24 | + create seed for `api`, so the metadata-admin engine renders a form rather than |
| 25 | + a raw-JSON textarea, and the entry carries a real label, domain and file |
| 26 | + patterns instead of the synthesised `label: 'api'`, `filePatterns: []` |
| 27 | + placeholder a type with no registry row gets. |
| 28 | + |
| 29 | +**The write door is unchanged.** `allowRuntimeCreate: true` records what the |
| 30 | +runtime already did: with no static registry entry, both write gates |
| 31 | +(`isRuntimeCreateAllowed`, `assertAllowed`) fall through to "runtime-creatable", |
| 32 | +and both name `api` in that comment. `allowOrgOverride` stays `false`, also its |
| 33 | +effective value today — an endpoint is the publishing package's outward URL |
| 34 | +contract, and a per-org fork could move `path`, flip `authRequired` or drop |
| 35 | +`rateLimit` on a URL third parties integrate against. Marking the type code-only |
| 36 | +instead (`allowRuntimeCreate: false` + `allowOrgOverride: false`) was considered |
| 37 | +and rejected: it would turn today's 200 into a 403 rather than validate it, and |
| 38 | +#5086's refusal runs before persistence for drafts too, which would leave |
| 39 | +#5206 step 2's `publishPackageDrafts` endpoint gate with no draft to gate. |
| 40 | + |
| 41 | +**`ApiEndpointSchema` gains the ADR-0010 protection envelope, and stays open to |
| 42 | +unknown keys.** Every registered kind must declare the envelope its loader |
| 43 | +stamps (`_packageId` / `_provenance`), or it is dropped on every parse; that |
| 44 | +spread is added. Closing the shape against unknown keys was attempted and |
| 45 | +**measured to be unsafe**: the same schema parses stored rows as well as |
| 46 | +authored declarations (`buildEndpointIndex`, `gateApiItemsForPublish`), and a |
| 47 | +stored row carries the metadata layer's own bookkeeping (`packageId`, `state`), |
| 48 | +so `strictObject` turned 10 tests in `packages/metadata` red — the load-time |
| 49 | +backstop excluded endpoints and the publish gate reported a schema error in |
| 50 | +place of its ADR-0121 D6 verdict. `api` therefore joins `view` on the #4001 |
| 51 | +campaign's `STILL_STRIP` list, with that measurement written into the list's own |
| 52 | +note, and the real fix (separating the stored envelope from the body at the |
| 53 | +metadata layer) is filed as #5309 rather than bought by teaching the authoring |
| 54 | +vocabulary two storage keys. |
| 55 | + |
| 56 | +**This is a shape check, not a second servability judge.** ADR-0121's rules — |
| 57 | +the `apps/<namespace>` carve-out (D1/D2), anonymous-requires-an-armed-`rateLimit` |
| 58 | +(D6), the supported target subset, mapping and policy — stay with |
| 59 | +`validateApiEndpointDeclarations` / `identityFreeEndpointGateFailure`, which run |
| 60 | +at publish and again at load. A pin test asserts an anonymous unmetered endpoint |
| 61 | +parses green here and is still refused by the gate, so the two never grow |
| 62 | +competing opinions. |
| 63 | + |
| 64 | +**Upgrade note (not purely additive).** A stored `api` row that does not satisfy |
| 65 | +`ApiEndpointSchema` is refused with 422 on its **next write**; reads and the |
| 66 | +existing load-time behaviour are unchanged (the matcher already excluded |
| 67 | +unparseable rows loudly, #5189). Every `api` declaration reachable in this repo |
| 68 | +— the two E8-migrated showcase endpoints and the two dogfood policy-fixture |
| 69 | +endpoints — was parsed against `ApiEndpointSchema` before landing this: all four |
| 70 | +clean. A live deployment's `sys_metadata` cannot be scanned from CI; an operator |
| 71 | +holding hand-written `api` rows should run `GET /api/v1/meta/diagnostics?type=api` |
| 72 | +(which now covers the type) before upgrading. |
| 73 | + |
| 74 | +ADR-0088's admission test is satisfied on all three clauses: independent |
| 75 | +lifecycle (the matcher indexes and invalidates one item at a time), declarative |
| 76 | +governability (`allowRuntimeCreate` plus file patterns), and a real consumer |
| 77 | +(#5040's executor, boot-proven by #5040 E8). This does not reverse the `router` |
| 78 | +kind's retirement — `router`'s delivered forms are code contributions, whereas a |
| 79 | +single `ApiEndpoint` is a declarative artifact, exactly the "third, real |
| 80 | +delivered form" ADR-0088's own `router` row anticipated. |
0 commit comments