Skip to content

Commit 73c76fb

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15204-s1-position-permission-sets
# Conflicts: # packages/spec/src/migrations/registry.ts
2 parents bf6bdf0 + ed1de8c commit 73c76fb

64 files changed

Lines changed: 5700 additions & 31295 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The migration registry `src/migrations/registry.ts` is generated at build and no longer committed
6+
7+
Clause-②: no
8+
9+
`src/migrations/registry.ts` keeps its path and its exports, and the built package carries the same
10+
`./migrations` entry as before. What changes is where the file comes from in this repository: it is
11+
git-ignored and written whole by `gen:migration-registry` from two committed sources,
12+
`src/migrations/registry.ts.template` (the hand-written step rationale, doc comments and table
13+
skeleton) and `src/migrations/entries/` (one file per entry). The generator runs on `pnpm install`
14+
(the package's new `prepare` script), as the first step of `build`, and before `typecheck`, `test`
15+
and `test:repo`; its self-test runs inside every generation. `check:migration-registry` is removed,
16+
since there is no committed copy left to compare.
17+
18+
For a contributor: add a migration entry as a file under `src/migrations/entries/` and edit any other
19+
registry prose in `registry.ts.template`. An edit to `registry.ts` itself is never committed, and the
20+
next install or build overwrites it.
21+
22+
Measured with `pnpm pack` against the previous build: 2,070 files each, and 4 differ. Those are
23+
`package.json`'s `scripts` (`build`, `typecheck`, `test` and `test:repo` now start with the generation,
24+
and `check:migration-registry` is gone; `pnpm pack` leaves `prepare` out of the packed manifest) and the
25+
three `.build-input-hash` stamps that record the build's inputs. Every `dist` file, declaration, source
26+
map and JSON Schema is byte-identical. No schema, type, export or runtime behaviour changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): `validateCapabilityReferences` resolves the capabilities a list view or a dashboard requires (#22639)
6+
7+
Clause-②: no
8+
9+
- The `/meta` read gate now serves a list view or a dashboard only to a user who holds every capability its `requiredPermissions` names, so a misspelled name hides the item from everyone. `validateCapabilityReferences` resolves those names like every other carrier's: a dashboard's, a view container's `list` and each `listViews` entry, a view item's `config`, a flattened list view's, and an object's own `listViews` entries. An unresolved name warns `capability-reference-unknown` at its own path (for example `views[0].listViews.legal_review.requiredPermissions`).
10+
- It is a warning, as for objects, fields, apps and actions: a single package's lint cannot see a capability another installed package declares.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/rest": minor
3+
---
4+
5+
feat(rest): the `/meta` read gate enforces a list view's and a dashboard's `requiredPermissions` (#22639)
6+
7+
Clause-②: yes (widening)
8+
9+
- **What the server now does.** `requiredPermissions` on a list view and on a dashboard is a list of capabilities a user must ALL hold, the same key and meaning as on an app, a navigation item and an action. The `/meta` read gate applies it through the one predicate the app arm uses (`holdsRequiredPermissions`), on both transports (`RestServer` and the runtime dispatcher's `/meta` domain):
10+
- `GET /api/v1/meta/view` and `GET /api/v1/meta/dashboard` list only what the caller may open. A view the caller does not hold is left out, so the console's view switcher (`?object=`) lists only the views the caller may open. A view container is listed minus the `list` / `listViews` entries the caller does not hold.
11+
- `GET /api/v1/meta/view/:name` and `GET /api/v1/meta/dashboard/:name` refuse a view or a dashboard the caller does not hold with `403 PERMISSION_DENIED`, the same refusal and envelope an app the caller may not open gets. Every alternate door of the read refuses it too (`/published`, `/layers`, `?layers=`, `?state=draft`, `/diff`, `/history`, `/audit`), to an author as to anyone.
12+
- `GET /api/v1/meta/object` and `/meta/object/:name` serve an object without the `listViews` entries the caller does not hold. A caller who may edit the object (the ADR-0106 D4 exemption the field mask already honours) reads every list view, so a save of what they loaded keeps them.
13+
- A view container is served whole to a caller who may write it on the stored-version doors (`/layers`, `?layers=`, `/diff`, `?state=draft`), as an app is, so a designer's save keeps the entries withheld from its author elsewhere.
14+
- **Caching.** The plain read's cached arm runs the same gate for `view` and `object`. Its `ETag` folds in what the gate withheld, and the conditional request is judged against that validator, so a caller who no longer holds a capability is never answered a `304` for the body that carried the gated view. When nothing is withheld the `ETag` is the same as before.
15+
- **Type surface.** `MetaItemReadRefusal` gains the `audience-permission` reason (`403 PERMISSION_DENIED`). A consumer that switches on `reason` writes it the way it writes `app-permission`.
16+
- **Nothing to migrate.** Only a list view or a dashboard that sets `requiredPermissions` is affected. The key was not accepted on either before the release that declared it.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
fix(runtime): the `/meta/<objectName>` legacy object exit asks the read gate before it serves the schema (#22639)
6+
7+
Clause-②: no
8+
9+
- The runtime dispatcher's one-segment `/meta/<objectName>` exit, reached when a host's protocol has no type catalogue and no list for the segment, served the object schema through the field mask alone. It now asks the shared `/meta` read gate first, as every other object exit does, so an object's own `listViews` entries whose `requiredPermissions` the caller does not hold are not served there either. A caller who may edit the object still reads them all.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): a list view's and a dashboard's `requiredPermissions` is enforced — the describe and the liveness ledger say so (#22639)
6+
7+
Clause-②: no
8+
9+
- The shared description of `ListViewSchema.requiredPermissions` and `DashboardSchema.requiredPermissions` drops its "not enforced yet" clause and says what the server does: a user who does not hold every capability is not listed the item and is refused it by name, a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object.
10+
- The liveness ledger rows (`liveness/view.json` `list.requiredPermissions`, `liveness/dashboard.json` `requiredPermissions`) are `live`, citing the `/meta` read gate, and no longer carry `authorWarn`, so `os lint` / `os validate` stop warning `liveness-planned-property` when a view or a dashboard sets the key.
11+
- The schemas themselves do not change: same shape, same accept set.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
An app, object or other item registered through `ObjectQL.registerApp` is now served with its package's `_packageVersion`, as the artifact loader already serves it
6+
7+
Clause-②: no
8+
9+
`_packageVersion` ("Owning package version") is declared on the protection envelope, and the metadata plugin's artifact loader stamps it. The registry load path stamped only `_packageId`: `ObjectQL.registerApp(manifest)` called `SchemaRegistry.registerItem` and `registerObject` with the package id alone. So `GET /api/v1/meta/app` served such an app with no version, and a console waiting for an upgrade could not tell the new app from the old one that a stale kernel still serves.
10+
11+
`registerApp` now stamps `manifest.version` on every item it registers: owned objects, object extensions, apps, every metadata collection, seed datasets, and the objects, app and collections of a nested plugin, which carry their owning package's version. A multi-package stack's residual top-level objects, which the engine registers under the stack's own id, now carry the stack's version too, as the metadata door stamps them. Each item is stamped before it reaches the registry, with the artifact loader's own call, `applyProtection(item, { packageId, packageVersion })`. The registry's stamp knows the id only, and it keeps a version that is already set. No signature changes: `SchemaRegistry.registerItem`, `registerObject` and `registerApp` keep theirs.
12+
13+
A caller that passes only a package id still stamps the id alone. No version is looked up or invented. Those callers are the metadata-service re-sync and the facade, which pass along the stamp the artifact loader already put on the item; the stored-row hydration, whose rows are tenant-authored; and the security catalog's built-ins.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os package install` no longer reports a hot install when the runtime answers `hotLoaded: false` (#22729)
6+
7+
Clause-②: no
8+
9+
- **What was wrong.** `os package install` printed "Package installed into the running kernel" for any `200` from `POST /api/v1/marketplace/install-local`. On the endpoint's lenient path (a catalog manifest whose hot-register throws), the runtime writes the package to its ledger and answers `200` with `hotLoaded: false` and the register error's message as `hotLoadError`: the package loads at the runtime's next restart, and the running kernel does not hold it. The command read neither key.
10+
- **What it prints now.** On `hotLoaded: false` it prints a warning that the package is installed but the running kernel could not load it, and that it loads at the runtime's next restart, followed by a `Hot-load error:` line carrying `hotLoadError` as the runtime answered it. When `hotLoadError` is missing, blank or not a string, no reason line is printed. The exit code stays `0`: the install itself succeeded.
11+
- **What did not change.** A hot-loaded install (`hotLoaded: true`) prints exactly as before, and so does an answer without `hotLoaded` (a runtime that predates the key). The command has no `--json` output, and none is added.

‎.gitattributes‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,10 @@
6464
# The migrations registry is the interesting one since #7297: its three append
6565
# tables now come from `packages/spec/src/migrations/entries/` (one file per
6666
# entry, the `.changeset/*.md` shape), so the conflict two retirement cards used
67-
# to have is gone at the SOURCE — different entries are different files. The
68-
# file stays out of this list anyway, because it is now MIXED: a driver that
69-
# deferred it whole would resolve its still-hand-written prose by regenerating,
70-
# which loses an edit rather than a merge. `check:migration-registry` is what
71-
# guards the generated half instead.
67+
# to have is gone at the SOURCE — different entries are different files. Since
68+
# #22554 the registry itself is git-ignored and generated whole at install and
69+
# build, so git never merges it; its hand-written prose lives in
70+
# `registry.ts.template`, which is plain source and text-merges like any other.
7271
#
7372
# The strictness ledger's COUNTS file joined at #5107 — the ledger's numbers were
7473
# the repo's hottest conflict surface and merged in the one way that hides: two

‎.github/workflows/lint.yml‎

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,11 @@ jobs:
283283
# over the ten merge-group runs measured for #16496; the PM dispatch-gates
284284
# self-test alone 597 s), and none of its expensive steps read a
285285
# merge group's file surface: a docs-only group paid the full battery.
286-
# So on `merge_group` and `pull_request` the TEN scoped families below
286+
# So on `merge_group` and `pull_request` the NINE scoped families below
287287
# -- each step carrying `if: steps.gate-families.outputs.<id> != 'skip'`
288288
# -- run only when the changed paths touch the files that family reads.
289-
# One of the ten, `migration_registry` (#19753), is a gate this job did
290-
# not run before at all; the other nine were scoped away from it.
289+
# (A tenth, `migration_registry` (#19753), retired with its gate in
290+
# #22554: the registry it compared is no longer committed.)
291291
# `push` on main and the scheduled full run keep the whole battery: the
292292
# script runs everything for any event it does not scope.
293293
#
@@ -399,24 +399,12 @@ jobs:
399399
- name: ADR anchors + number uniqueness (governed code names its decision)
400400
run: pnpm check:adr-anchors
401401

402-
# Migration registry matches its entry files (#19753). `registry.ts` is
403-
# the generated concatenation of `packages/spec/src/migrations/entries/`,
404-
# it ships in the spec package's `dist`, and this check is the only
405-
# proof the two agree -- `check:generated --reconcile-only` (in the
406-
# `Type Check · source gates` job) proves the script is classified,
407-
# never that the artifact is current. It ran
408-
# in no CI step, so an entry edited without `gen:migration-registry`
409-
# shipped the old text with every required check green.
410-
#
411-
# Scoped, on the maintainer's ruling for that card: a step in THIS
412-
# required job, run when the change touches what the check reads
413-
# (`migration_registry` in the selector above). ⛔ Not a separate
414-
# `paths:`-filtered job: one that never reports on an unrelated PR
415-
# leaves a required context pending forever. Cheap -- tsx over one
416-
# directory, no build (~1.4 s measured locally) -- so it sits early.
417-
- name: Migration registry matches its entry files
418-
if: steps.gate-families.outputs.migration_registry != 'skip'
419-
run: pnpm --filter @objectstack/spec check:migration-registry
402+
# No "migration registry matches its entry files" step since #22554:
403+
# `packages/spec/src/migrations/registry.ts` is git-ignored and generated
404+
# whole from `registry.ts.template` + `entries/` by `gen:migration-registry`,
405+
# which `pnpm install` above already ran (spec's `prepare`, self-test
406+
# included). There is no committed copy left to compare, and an entry the
407+
# generator refuses fails that install step, loudly, in every job.
420408

421409
# Enforces the no-restricted-imports guard against @objectstack/spec root
422410
# namespace imports (the dormant rule was never run in CI). Syntactic

‎.gitignore‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ packages/*/*.tsbuildinfo
6262
# Generated JSON Schema (rebuilt by `pnpm gen:schema` during build)
6363
packages/spec/json-schema/
6464

65+
# Generated migration registry (rebuilt by `pnpm gen:migration-registry` on
66+
# install and as the first step of build, from registry.ts.template + entries/),
67+
# and the staging file its atomic write renames into place.
68+
packages/spec/src/migrations/registry.ts
69+
packages/spec/src/migrations/registry.ts.*.tmp
70+
6571
# Generated bundle size reports
6672
packages/spec/bundle-size-report.json
6773

0 commit comments

Comments
 (0)