Skip to content

Commit 9fa1ff4

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20216-view-container-object-refused
2 parents 94439cd + c02fa12 commit 9fa1ff4

21 files changed

Lines changed: 1757 additions & 76 deletions
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
'@objectstack/runtime': minor
3+
---
4+
5+
fix(runtime): `POST /api/v1/packages` parses the whole body through `PackageInstallBodySchema` instead of reading it key by key (#19328)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING for callers of the install door.** The door now enforces the
10+
WHOLE install-body declaration. A body that `PackageInstallBodySchema` refuses
11+
is refused with `400` / `VALIDATION_ERROR` and installs nothing, whatever the
12+
reason. Every shape below used to answer `201`, and two of the install options
13+
used to be honoured. The list names the shapes a caller is most likely to have
14+
sent, and ends with one bullet for everything else the declaration states. For
15+
each one, change what you send FROM the refused shape TO the declared one:
16+
17+
- **A manifest with no `type`, in either body form.** FROM
18+
`{ "manifest": { "id": …, "name": …, "version": … } }` (or the same manifest
19+
sent bare) TO the same manifest with a declared `type`: one of `app`,
20+
`plugin`, `ui`, `driver`, `server`, `theme`, `agent`, `objectql`, `module`,
21+
`gateway` or `adapter`, e.g. `"type": "app"`. It used to install anyway.
22+
- **A manifest with no `name`, in either body form.** `name` is the manifest's
23+
other required key besides `id`, `version` and `type`. FROM
24+
`{ "manifest": { "id": …, "version": …, "type": … } }` (or the same manifest
25+
sent bare) TO the same manifest with `"name": "…"`, the human-readable
26+
package name. It used to install anyway, because the door parsed only the
27+
`id` and `version` legs.
28+
- **An unknown key inside the manifest, or on a bare body.** Example: a
29+
transposed `namesapce`. FROM a manifest carrying the undeclared key TO the
30+
manifest with that key removed, or spelled as the declared key it was meant
31+
to be (`namespace`). The refusal names the key. The key used to be STORED
32+
with the package.
33+
- **A string-typed `enableOnInstall` or `overwrite`.** FROM
34+
`"enableOnInstall": "false"` / `"overwrite": "true"` TO JSON booleans,
35+
`"enableOnInstall": false` / `"overwrite": true`. `'false'` used to install a
36+
fresh package ENABLED, which is the opposite of what the caller asked for.
37+
`'true'` for `overwrite` was read as absent, which answered `409` on an
38+
installed id.
39+
- **Install options spelled on the BARE form (`enableOnInstall`, `overwrite`,
40+
`settings`).** FROM `{ "id": …, …, "overwrite": true }` TO the wrapped form,
41+
`{ "manifest": { "id": …, … }, "enableOnInstall": …, "overwrite": …,
42+
"settings": … }`. `overwrite` may also go on the query string instead
43+
(`?overwrite=true`), which a bare body may keep using. Before this change the
44+
door handled these key by key: `enableOnInstall` was ignored, but
45+
**`overwrite: true` and `settings` were HONOURED** (an installed id was
46+
overwritten, and the settings reached the install). All three were also
47+
stored as manifest keys. They are refused now. This is the part of the
48+
change that removes behaviour a caller could have been relying on.
49+
- **Any other constraint `ManifestSchema` declares.** Each one is now enforced
50+
at this door. Before, only the `id` and `version` legs were, and the manifest
51+
was stored as sent. The constraints include:
52+
- the `namespace` grammar: 2–20 characters, starting with a lowercase
53+
letter, with only lowercase letters, digits and underscores;
54+
- the closed value sets of `scope` (`cloud`, `system`, `project`),
55+
`runtime` and `packaging`;
56+
- the declared type of a key (for example a non-string `description`, or a
57+
non-string version in `dependencies`);
58+
- the retired manifest keys `configuration`, `capabilities`, `extensions` and
59+
`loading`, which the declaration keeps only as named refusals;
60+
- unknown keys inside the nested blocks the declaration closes. These are
61+
`contributes` (and its `kinds[]`), `data[]` (each entry is a `SeedSchema`
62+
seed), `navigationContributions[]` (and their items), `engine`, `engines`,
63+
and the structured form of `permissions`.
64+
65+
FROM the off-declaration value TO the value the declaration states. The
66+
refusal names the path.
67+
68+
The accept set only shrinks back to what the published declaration has always
69+
said. `PackageInstallBodySchema` in `@objectstack/spec` is a union of two
70+
branches: the wrapped request, or a bare manifest as the whole body.
71+
`ManifestSchema` declares the manifest's required keys (`id`, `name`,
72+
`version`, `type`) and the grammar or value set of the rest. It closes the
73+
manifest, and the nested blocks listed above, against unknown keys.
74+
The wrapped request types `enableOnInstall` and `overwrite` as booleans. Its
75+
docblock says a bare manifest carries no install options: «a caller that needs
76+
an option sends the wrapped form». Until now, the door parsed only two legs of
77+
the manifest (`id` and `version`) and read every other key positionally off
78+
the raw body. That is «declared ≠ enforced» on a published API contract. Now
79+
the door parses the body once through the declared union and reads
80+
`overwrite`, `settings` and `enableOnInstall` off the parsed request. Nothing
81+
in `@objectstack/spec` moves, and neither branch of the union is relaxed.
82+
83+
**What is not affected.**
84+
85+
- A well-formed body installs exactly as before, in both forms and on both
86+
install limbs (the protocol primitive and the bare-registry fallback).
87+
- The first-party SDK (`client.packages.install(m, { enableOnInstall,
88+
overwrite, settings })`) already sends the wrapped form with JSON booleans.
89+
Studio's create-package dialog sends `{ manifest }` with a declared `type`.
90+
- The manifest is stored as sent, with no parse-time defaults added.
91+
- The refusals for a missing or malformed `id` and `version` keep their own
92+
sentences and still come first.
93+
- Every request-shape refusal is still answered ahead of the duplicate-id
94+
`409`.
95+
- Boot-time and in-process installs reach `SchemaRegistry.installPackage` /
96+
`ObjectQL.registerApp` directly and never pass through this door.
97+
98+
**Still accepted, because the declaration accepts it.** An unknown key at the
99+
top level of the wrapped form (`{ manifest, bogus }`) is dropped, not refused.
100+
`PackageInstallRequestSchema` is declared in strip mode, and the door now does
101+
exactly what the declaration says.
102+
103+
**If you are refused.** The refusal names the path of each failed constraint,
104+
and the form it read the body as. A misplaced install option also gets the wrapped form (and, for
105+
`overwrite`, `?overwrite=true`) spelled out. So the prescription arrives with
106+
the `400` rather than in a changelog.
107+
108+
<!-- 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 — every refused shape is one the published declaration already refused, and a caller repairs it by sending the declared shape (a declared `type`, a `name`, the declared key or value, a JSON boolean, the wrapped form). The refusal itself carries the remedy. -->
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: retire the view item's `owner` and `hidden` keys — declared, stored, and read by nothing (#20085)
6+
7+
**BREAKING** — `owner` and `hidden` are removed from the view item
8+
(`defineViewItem`, `ViewItemSchema`, and the `view` metadata door's ViewItem
9+
record `{ name, object, viewKind, config }`). ADR-0049 enforce-or-remove; triage
10+
direction, verbatim: 「retire both keys」.
11+
12+
Both keys were accepted by the strict authoring door and by the wire member the
13+
`PUT /api/v1/meta/view` door validates, and `saveMetaItem` stored them verbatim —
14+
but nothing ever read or wrote either. Measured before removal, each against a
15+
lit control: no reader or writer of the view-item keys in the framework, in
16+
objectui at its pinned commit and at `main`, or in cloud. Both view-switcher read
17+
paths (`GET /meta/view?object=` and `getViewsByObject`) filter on `viewKind` +
18+
`object` and sort on `order`. So `hidden: true` hid nothing, and a view with
19+
`owner` set was listed for every user who can read the object. The `owner` half
20+
was a visibility claim nothing enforced, which is the security shape ADR-0049 is
21+
about. Per-user view scoping is a parked direction (ADR-0017, amended
22+
2026-09-04), not a shipped mechanism.
23+
24+
### FROM → TO
25+
26+
| removed | what to write instead |
27+
| --- | --- |
28+
| view item `owner` | delete the key. Nothing restricts a view item to one user today; a view item is visible to everyone who can read its object. |
29+
| view item `hidden` | delete the key. To take a view out of the switcher, delete the view item (or stop shipping it from source). |
30+
31+
**The one-line fix: delete `owner:` and `hidden:` from every view item.**
32+
`os migrate meta --from 17` lists the mechanical edits for existing sources.
33+
34+
⚠️ Runtime behaviour is deliberately **unchanged**. Neither key ever changed what
35+
a view showed or to whom, so removing one removes no behaviour. What changes is
36+
the answer an author gets: a view item carrying either key is now refused at
37+
parse, with a prescription, instead of being saved with no effect.
38+
39+
### The retirement kit
40+
41+
- **Tombstones, on the shared shape.** Both keys are `retiredKey()` tombstones on
42+
the view-item base shape. That shape feeds two doors: the strict authoring door
43+
(`ViewItemSchema`) and the `.strip()` wire member (`ViewItemWireSchema`, which
44+
the `view` write door and the assembled-manifest `viewItems` channel both run).
45+
A bare deletion there would have been a silent strip (ADR-0104), so one
46+
tombstone serves both: `tsc` types the key `never` on `defineViewItem`'s input,
47+
and every parse raises the prescription.
48+
- **D2 conversion `view-item-owner-hidden-removed`** (step 18, retired from the
49+
load path). It strips both keys from the view item **record** spelling as a
50+
lossless delete, in both collections a record travels in: `views` (stack
51+
sources, and stored `sys_metadata` rows, which the rehydration seam replays as
52+
`{ views: [row] }`) and the assembled-manifest `viewItems` channel (package
53+
export and environment artifacts). Without the second, an artifact assembled
54+
before this release would fail its registration parse.
55+
- **`RETIRED_KEYS_BY_MAJOR[18]`**: `ui/ViewItem:owner`, `ui/ViewItem:hidden`,
56+
`ui/ViewItemWire:owner`, `ui/ViewItemWire:hidden`.
57+
- **No liveness row, and no authorable-surface line.** Both instruments read a
58+
def's top-level `properties`, and `ViewItem` / `ViewItemWire` are
59+
discriminated unions that have none. So the four surface ratchets stay
60+
byte-identical on this retirement, and that reading is expected on this route.
61+
- **No deprecation window**, per the project's startup-stage posture.
62+
63+
⛔ **Untouched: the flattened-overlay members' own `owner` / `hidden`.** Those
64+
two members of the `view` door (a lean personalization PUT carrying no `config`)
65+
declare the same names on a different door, which this change does not touch. A
66+
`{ object, viewKind, hidden: true }` overlay still parses. The conversion leaves
67+
overlays alone for the same reason.
68+
69+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec`
70+
is published, so this is breaking for consumers no telemetry was consulted for.
71+
72+
Clause-②: no
73+
74+
<!-- adr-0087: registered view-item-owner-hidden-removed -->

0 commit comments

Comments
 (0)