|
| 1 | +--- |
| 2 | +"@objectstack/spec": major |
| 3 | +"@objectstack/metadata": patch |
| 4 | +--- |
| 5 | + |
| 6 | +feat(spec)!: declarative `apis:` publishes again — the blanket refusal narrows to per-endpoint publish gates, and declared endpoints go LIVE (#5111, #5040 E7) |
| 7 | + |
| 8 | +⚠️ **Read this as a security note, not a schema note.** Declarative endpoints |
| 9 | +**execute** from protocol 17. Before this release the surface was inert end to |
| 10 | +end — nothing mounted a declared `path`, no matcher existed, and every key |
| 11 | +including `authRequired` parsed green and gated nothing — which is why #4936 |
| 12 | +refused a non-empty `apis:` outright. The #5040 E-series built the executor |
| 13 | +(mount seam, endpoint matcher, policy keys, execution targets, mapping keys, |
| 14 | +OpenAPI enrichment), so the refusal's premise is gone and keeping it would be |
| 15 | +the lie in the other direction. |
| 16 | + |
| 17 | +## BREAKING — the refusal narrows, and what passes it is served |
| 18 | + |
| 19 | +`apis: [ …endpoints… ]` no longer fails wholesale. Each entry is now gated |
| 20 | +individually, and **an endpoint that passes the gate is mounted and answers |
| 21 | +real requests as soon as the stack is published.** |
| 22 | + |
| 23 | +**Before you upgrade, review every historical `apis:` block** — including any |
| 24 | +you restored, generated from an older doc, or left in place because it was |
| 25 | +known to do nothing. Pay particular attention to any entry that explicitly |
| 26 | +declares **`authRequired: false`**: the schema default is `true`, so an |
| 27 | +*omission* is safe and needs no review, while an explicit `false` is the only |
| 28 | +thing that opens **anonymous** access to that endpoint. ADR-0121 D6 now pairs |
| 29 | +it with a mandatory armed rate limit — and "armed" means |
| 30 | +`rateLimit: { enabled: true, … }`, because `enabled` defaults to `false`, so a |
| 31 | +budget written without it meters nothing. |
| 32 | + |
| 33 | +## The gates, each rejecting with its own prescription |
| 34 | + |
| 35 | +| gate | rejected shape | |
| 36 | +|---|---| |
| 37 | +| **namespace** (ADR-0121 D1/D2) | a `path` that is not `/api/v1/apps/<manifest.namespace>/<subpath>`, or a stack that declares `apis:` without an explicit `manifest.namespace` (no derivation from `manifest.id`) | |
| 38 | +| **supported subset** | `type: 'script'` / `'proxy'`; an `object_operation` missing `objectParams.object` or `.operation`; a `flow` with an empty `target` | |
| 39 | +| **mapping** | any `transform`; an unusable `source`/`target` path (empty, empty segment `a..b`, `__proto__`/`prototype`/`constructor`); two entries whose `target`s collide (same path, or one inside another); `inputMapping` on a `find`/`get`/`delete` operation, which never reads a body | |
| 40 | +| **policy** | `authRequired: false` without `rateLimit.enabled === true`; an armed budget with `maxRequests`/`windowMs` ≤ 0; a negative `cacheTtl`; `cacheTtl` on a non-GET method | |
| 41 | +| **uniqueness** | two endpoints in one stack claiming the same METHOD + path (one trailing slash trimmed, the matcher's own rule) | |
| 42 | + |
| 43 | +**FROM → TO.** `path: '/api/v1/<anything>/thing'` → |
| 44 | +`path: '/api/v1/apps/<manifest.namespace>/thing'`, with `manifest.namespace` |
| 45 | +declared explicitly. `authRequired: false` → either delete the key (the safe |
| 46 | +default `true` applies) or keep it **and** add |
| 47 | +`rateLimit: { enabled: true, windowMs: 60000, maxRequests: 100 }`. Every other |
| 48 | +key is unchanged: the `ApiEndpoint` vocabulary is frozen — this release adds, |
| 49 | +removes and renames nothing on it. The gates are validation logic over the keys |
| 50 | +that already existed. |
| 51 | + |
| 52 | +The runtime keeps its own refusals for a declaration that reached the store |
| 53 | +without passing publish (a direct `metadata.register()`), so the two ends agree: |
| 54 | +what publish accepts is exactly what the executor serves. |
| 55 | + |
| 56 | +`normalizeEndpointPath` is now exported from `@objectstack/spec/api` and is the |
| 57 | +one canonical form of a declared path — the publish gate and the endpoint |
| 58 | +matcher (`@objectstack/metadata`) read the same rule instead of each carrying a |
| 59 | +copy, so a stack can never publish a duplicate the matcher would silently |
| 60 | +resolve to a single winner. |
0 commit comments