|
| 1 | +--- |
| 2 | +'@objectstack/runtime': minor |
| 3 | +--- |
| 4 | + |
| 5 | +fix(runtime): `POST /api/v1/packages` parses the manifest's `id` leg instead of reading it positionally (#19417) |
| 6 | + |
| 7 | +Clause-②: no (narrowing) |
| 8 | + |
| 9 | +**BREAKING for callers of the install door** — a manifest whose `id` is not |
| 10 | +reverse-domain notation is now refused `400` / `VALIDATION_ERROR`. It used to |
| 11 | +install and answer `201`. |
| 12 | + |
| 13 | +The accept set only shrinks back to what the published declaration has always |
| 14 | +said. `MANIFEST_ID_PATTERN` is declared once in |
| 15 | +`packages/spec/src/kernel/manifest.zod.ts` and referenced by both faces of one |
| 16 | +identity — `ManifestSchema.id`, what an author writes, and |
| 17 | +`PackageSchema.manifestId`, what the registry stores and publishes by. The door |
| 18 | +read `manifest.id` POSITIONALLY (`typeof manifest?.id === 'string' ? |
| 19 | +manifest.id.trim() : ''`) and parsed nothing, so `id: 'pkg-a'` installed and |
| 20 | +answered `201` while `defineStack()`, `os build`, `os validate` and the publish |
| 21 | +face all refused the same id. The author was handed a package that could never |
| 22 | +be rebuilt or published. That is «declared ≠ enforced» on a published API |
| 23 | +contract — and nothing in `packages/spec` moves for it: the declaration was |
| 24 | +already right. |
| 25 | + |
| 26 | +The gate asks the declaration **by reference** — `ManifestSchema.shape.id` — |
| 27 | +rather than keeping a copy of the grammar, exactly as the `version` leg beside |
| 28 | +it does, so a future move of the reverse-domain rule reaches this door with no |
| 29 | +further edit. The sentence the caller reads is the declaration's own |
| 30 | +(`manifestIdRefusal`), **surfaced rather than reworded**: it names the key, |
| 31 | +echoes the value, lists the two examples, and carries a suggestion arm that |
| 32 | +verifies its candidate against the pattern before offering it. Posting |
| 33 | +`id: 'pkg-a'` now answers, in the response envelope's `error.message`: |
| 34 | + |
| 35 | +```text |
| 36 | +Invalid package id 'pkg-a' on `manifest.id`. Expected reverse-domain notation |
| 37 | +('com.steedos.crm', 'org.apache.superset') — lowercase dot-separated segments; |
| 38 | +hyphens allowed inside a segment, underscores are not. Did you mean |
| 39 | +'com.example.pkg-a'? |
| 40 | +``` |
| 41 | + |
| 42 | +**What is not affected.** Boot-time and in-process installs reach |
| 43 | +`SchemaRegistry.installPackage` / `ObjectQL.registerApp` directly and never pass |
| 44 | +through this branch, so nothing about how a package is loaded from disk or |
| 45 | +registered by a plugin changes. A conforming manifest installs exactly as |
| 46 | +before, on both body forms (wrapped and bare) and on both install limbs (the |
| 47 | +protocol primitive and the bare-registry fallback). |
| 48 | + |
| 49 | +**The `Package id is required` sentence does NOT move.** `''` fails |
| 50 | +`MANIFEST_ID_PATTERN` too, so where this gate sits decides whether a published |
| 51 | +message changes or only the accept set does. It is ordered AFTER the existing |
| 52 | +`!pkgId` check: an absent, empty, whitespace-only or non-string `id` still |
| 53 | +answers `400 Package id is required`, never the schema's sentence — which on |
| 54 | +that input is the one case where the suggestion arm has nothing to offer. |
| 55 | +Measured both directions. It is ordered BEFORE the `version` gate for the |
| 56 | +mirror-image reason: that refusal's sentence names the id it prescribes for, and |
| 57 | +prescribing a `version` repair for an id that can never be legal sends the |
| 58 | +author round twice. |
| 59 | + |
| 60 | +**Scope — the `id` leg alone.** The declaration's residual docblock records the |
| 61 | +classes this door answers `201` to while `PackageInstallBodySchema` refuses |
| 62 | +them. This change closes one: `id`. A missing `type`, unknown keys on either |
| 63 | +body form, a string-typed `enableOnInstall` / `overwrite`, and install options |
| 64 | +spelled on the bare form are each left exactly as they were — measured after the |
| 65 | +change, all seven spellings of those four classes still answer `201`, against a |
| 66 | +`pkg-a` control that answers `400` and a conforming control that answers `201`. |
| 67 | +Each is its own reading and its own card. Closing them is the one call this |
| 68 | +handler still pointedly does not make, `PackageInstallBodySchema.safeParse(body)`. |
| 69 | + |
| 70 | +**If you are refused.** Give the manifest an id in reverse-domain notation — |
| 71 | +lowercase dot-separated segments, hyphens allowed inside a segment, underscores |
| 72 | +not. The refusal names the key, echoes what you wrote and, where a mechanical |
| 73 | +repair exists, offers one it has already checked against the rule, so the |
| 74 | +prescription arrives with the `400` rather than in a changelog. |
| 75 | + |
| 76 | +<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or reshaped: no spec key, no export, no stored row. `objectstack migrate meta` has nothing to reach, because there is no old spelling that maps to a new one — a caller supplies an id the declaration already required, and an id's repair changes the package's identity, so no mechanical mapping could be prescribed even in principle. The refusal itself carries the remedy. --> |
0 commit comments