Skip to content

Commit a0901f1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/os-serve-artifact-startup-cgy5aq
2 parents 7d292fa + 4dc14cc commit a0901f1

48 files changed

Lines changed: 2067 additions & 469 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/adapter-hono": patch
3+
---
4+
5+
fix(adapters/hono): the auth wildcard yields paths the auth service does not own (#4117)
6+
7+
`app.all('${prefix}/auth/*')` claimed a whole namespace and was **terminal**: it
8+
returned the auth service's response unconditionally, including better-auth's 404
9+
for a path it does not implement, and the legacy `handleAuth` bridge's own
10+
`handled: false` 404. That is the #4088 shape, found by #4116's enumeration after
11+
manual greps had missed it.
12+
13+
A 404 from better-auth, or `handled: false` from the dispatcher, now means "not
14+
this mount's path" and the handler yields. The predicate is the dispatcher's own
15+
`handled` flag wherever one exists — an explicit ownership answer beats inferring
16+
one from a status; only the better-auth hand-off lacks such a flag, and there the
17+
404 is the signal, as in #4092.
18+
19+
**What changes on the wire.** An unowned path under `${prefix}/auth/*` used to get
20+
a 404 built by this mount. It now continues to the `${prefix}/*` dispatcher
21+
catch-all and gets a real, gate-carrying `dispatch()` attempt, so a domain handler
22+
registered for such a path becomes reachable — this adapter's actual extension
23+
mechanism. When nothing anywhere claims the path the reply is still the same
24+
enveloped `{ success: false, error: { message: 'Not Found', code: 404 } }`. Paths
25+
the auth service does own are untouched, and a 401/403 from it is never treated as
26+
a disclaimer of ownership.
27+
28+
No configuration changes and no new routes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/plugin-dev': minor
3+
'@objectstack/plugin-hono-server': patch
4+
---
5+
6+
Retire the three `security.*` dev stubs, and refuse to load `plugin-dev` under `NODE_ENV=production` (#4093).
7+
8+
**The security stubs are gone.** When `@objectstack/plugin-security` was not installed, `plugin-dev` filled its three slots with fakes that inverted the decision each stood in for: `security.permissions.checkObjectPermission()` returned `true` for everything, `security.rls.compileFilter()` returned `null` so no row-level predicate was applied, and `security.fieldMasker.maskResults()` returned rows unmasked. ADR-0076 D12's rule — learned from the analytics shim it retired in #3891 — is that a fallback may degrade features, **never security semantics**; `packages/spec/src/contracts/security-service.ts` says the same from the other side (these three are plugin-security's internals, and access-narrowing answers must fail CLOSED). Since `plugin-dev` loads SecurityPlugin through the same optional dynamic import as everything else, the package merely being absent was enough to swap real RBAC/RLS/masking for allow-all behind a single `warn` line.
9+
10+
The slots now stay empty — which is what production has without SecurityPlugin, and what every consumer already handles — and the boot log states plainly that RBAC, row-level security and field masking are not being enforced.
11+
12+
**`plugin-dev` now refuses to initialize under `NODE_ENV=production`.** It is a published package that registers development fakes for every unclaimed core service slot, including ones that report success for work they never did, and it had no environment check of its own: an `objectstack.config.ts` carrying `new DevPlugin()` into a production deploy got the whole fake slate with only a boot log to say so. `init()` now throws there. Set `OS_ALLOW_DEV_PLUGIN=1` if you deliberately want the dev slate under a production `NODE_ENV` (a staging box mimicking prod, a smoke test that pins the variable).
13+
14+
FROM → TO: a stack that relied on the dev security stubs was not being protected by them — it was being told everything was allowed. Install `@objectstack/plugin-security` to enforce RBAC/RLS/masking, or accept the empty slots (unchanged behaviour on every path that already handled an absent SecurityPlugin). A production process that loaded `plugin-dev` must now either drop it and install the real services, or opt in explicitly with `OS_ALLOW_DEV_PLUGIN=1`.
15+
16+
Also: `plugin-hono-server`'s `/auth/me/permissions` resolves `security.permissions` and `metadata` through the same guarded lookup its three sibling lookups already used. An unregistered slot makes `getService` throw, which previously landed in the outer catch — the same fail-open response body, but logged as "/auth/me/permissions failed" on every console navigation instead of taking the deliberate `!evaluator` branch.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/runtime': patch
4+
---
5+
6+
Both discovery builders now derive the `data` service entry from the implementation in the slot, closing the hardcoded "kernel-provided" block (#4130).
7+
8+
#4089 computed `metadata`; `data` was the last entry that judged itself, reporting `status: 'available'` and `handlerReady: true` unconditionally. That was true — but by a convention in a different package, not by anything either builder checked: ObjectQL is the slot's only producer, and plugin-dev always loads `ObjectQLPlugin` as a child, so plugin-dev's `data` stub (`find()` returns `[]`, `insert()` mints an id and stores nothing) never reaches the slot. A second producer, or a trimmed dev config, and the hardcode starts lying about the platform's most load-bearing capability.
9+
10+
Both builders now read the registered service's `__serviceInfo`:
11+
12+
- a real engine carries no marker ⇒ `available` + `handlerReady: true`, byte-identical to the hardcode it replaces (verified on a real kernel boot);
13+
- a self-declared stub ⇒ its own `status` and `message`, with `handlerReady: false` (the default for `stub`), so a consumer that gates on `handlerReady` stops treating an empty query engine as a real one.
14+
15+
`handlerReady` is derived here rather than pinned `true` as it is for `metadata`, because the two routes differ: `/meta` answers from the protocol whatever fills the metadata slot, while `/data` needs the `protocol` or an objectql-shaped service and 503s without them — and the only stack where a stub occupies the `data` slot is one where ObjectQL never registered. No routing, gating or dispatch behavior changes: the `data` domain resolves its engine directly and never consulted this slot.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
---
3+
4+
Docs only — no package changes, nothing to release.
5+
6+
Records why `packages/*/CHANGELOG.md` is **not** added to the docs-drift mapper's test-file
7+
exclusion (#4091). It reads as the obvious next narrowing and is a provable no-op:
8+
9+
- `chore: version packages` is the only PR class that mass-touches those files, and it runs
10+
no GitHub Actions — `changesets/action` opens it with the repo `GITHUB_TOKEN`, which by
11+
design triggers no workflow runs. Measured on #3910: one check run, Vercel's own app.
12+
The version bump is still verified, just post-merge (`ci.yml` / `lint.yml` on `push: main`,
13+
and `release.yml` gates publish on a green build).
14+
- `changeset version` writes `package.json` beside every `CHANGELOG.md` it appends to
15+
(45 vs 46 on page 1 of #3910's diff), so excluding the CHANGELOGs would leave the derived
16+
package-root set bit-identical anyway.
17+
18+
Written down so the next reader does not spend a PR rediscovering it as a gap.

.changeset/i18n-view-coverage.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/cli": patch
3+
"@objectstack/spec": patch
4+
---
5+
6+
fix(cli,spec): i18n coverage actually gates view labels — the `defineView()` container is no longer skipped (#4123)
7+
8+
`i18n/missing-view` had **zero producers**. `collectExpectedEntries` recognized
9+
two view shapes and the compiled config is neither:
10+
11+
1. **Object-nested `listViews`** — objects do not carry `listViews` once
12+
compiled (0 across every example).
13+
2. **Top-level named views** — guarded by `if (!view?.name) continue`.
14+
15+
`defineView()` emits the aggregated View **container**, `{ list, listViews,
16+
formViews }`, which per spec (`view.zod.ts`) has **no top-level `name`**: it is
17+
keyed implicitly by its target object at `list.data.object`, exactly as
18+
objectql's `resolveMetadataItemName` resolves it. So the guard rejected the
19+
spec's own container shape, and with it every view in every example — 64 view
20+
strings that the ratchet reported as fully covered.
21+
22+
The walker now handles the container, emitting under the same
23+
`objects.<object>._views.<view>.*` convention the runtime resolver reads
24+
(`viewLabel` in `@object-ui/i18n`) and the shipped platform bundles already
25+
carry. An unnamed default `list` resolves under `_views.list`, matching the
26+
console's `primary.name || 'list'`. `formViews` stays uncovered — form views
27+
have no counterpart in that resolver convention, so keys for them would expect
28+
translations nothing reads.
29+
30+
`StrictObjectTranslation` gains the `_views` slot that
31+
`ObjectTranslationDataSchema` already permits. Without it, `satisfies
32+
StrictObjectTranslation<…>` rejects the very translations the gate now asks
33+
for.
34+
35+
The newly surfaced strings are **translated, not ratcheted** (the precedent set
36+
when the object-less action landed): `check-i18n-coverage` stays at 665 with
37+
none new.

.changeset/inline-action-schema.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `element:button` declares the action it executes — `InlineActionSchema` (objectui#2997)
6+
7+
`element:button`'s renderer reads `properties.action` and dispatches it through
8+
the `ActionRunner`; that prop is the only thing making a standalone-page button
9+
interactive. `ElementButtonPropsSchema` declared `label`, `variant`, `size`,
10+
`icon`, `iconPosition`, `disabled`, `aria` — and no `action`.
11+
12+
It is being authored anyway. cloud's `service-tenant` pages carry five
13+
declaration sites across the billing and pricing funnel (`pricing`, `welcome`,
14+
`billing-cancel` ×2, `billing-success`), each `{ type: 'navigation', to: … }`,
15+
each cast `as any` to get past the type system.
16+
17+
**An undeclared prop is stripped, not ignored.**
18+
`ElementButtonPropsSchema.safeParse({ label, action })` returned `success: true`
19+
with no `action` in `data` — a non-strict `z.object` drops unknown keys. That is
20+
harmless only because page block `properties` are still
21+
`z.record(z.string(), z.unknown())`, so this schema never runs on a real page
22+
save. The moment anyone tightens `properties` to validate against
23+
`ComponentPropsMap` — an obvious hardening — every one of those buttons loses its
24+
action at save, with a green parse. Declaring the prop is what defuses it.
25+
26+
**`InlineActionSchema` is derived, not a new dialect.** `ActionSchema` is
27+
`z.object(…).refine(…).refine(…)`, so `.pick()` is unavailable on the exported
28+
schema — the object half is now a factory both schemas build from, which keeps
29+
`lazySchema`'s deferral (the fields are constructed on first use of whichever
30+
schema is touched, not at module load). The inline schema `.pick()`s the twelve
31+
fields `element:button` actually forwards to the runner, so their `describe()`
32+
text, the `ActionType` vocabulary and the `target`-required refinement are shared
33+
rather than restated.
34+
35+
`name` and `label` are optional, which is the substantive difference:
36+
`ActionSchema` requires both because a registry entry needs an identity and a
37+
menu label, and an inline action has neither — the button already has its own
38+
`label`, and requiring `action.label` too would mean writing it twice. Everything
39+
that only means something for a registered action — `objectName`, `locations`,
40+
`order`, `ai`, `requiredPermissions`, `visible`/`disabled`, `resultDialog` — is
41+
excluded, as are `icon`/`variant` (the button has its own) and `body` (a page
42+
button running an inline sandboxed script is a separate decision). A declared
43+
field no renderer reads is the failure this schema exists to stop, so widen it
44+
when a renderer widens, not before.
45+
46+
**`navigation` and `to` become normalizing aliases, not new members.**
47+
`normalizeInlineAction` folds `type: 'navigation'``'url'` and `to``target`
48+
on parse, the `VIEW_FILTER_OPERATOR_ALIASES` pattern. So cloud's existing pages
49+
keep validating unchanged while parse output is always canonical, and the aliases
50+
get a `retiredKey` tombstone once the producers are migrated. `url` is also the
51+
*better* target: the runner's `url` path has `${param.X}` / `${ctx.X}`
52+
interpolation, `apiBase` promotion for `/api/…` paths and popup-blocker-safe
53+
`openIn`, none of which its `navigation` path has.
54+
55+
Deliberately **not** done: promoting `navigation` into `ActionType` as a bare
56+
member, which was considered and declined in #4070. It would name the type while
57+
leaving the prop undeclared and `to` homeless — half a fix, and a permanent
58+
synonym in a vocabulary whose members cannot be removed later.
59+
60+
Nothing is narrowed. `ui/InlineAction` and `ui/ElementButtonProps:action` are
61+
additions to every generated surface; no accepted value stops being accepted, so
62+
no stored page metadata changes meaning.
63+
64+
Verified: 16 new tests — the derivation is asserted from both directions
65+
(the inline field set is exactly the documented twelve; every one of them
66+
round-trips through `ActionSchema`; every registry-only concern is absent; every
67+
`ActionType` member is inline-authorable), and the fold is pinned against the
68+
literal shape cloud writes. Full `@objectstack/spec` suite **7038 tests across
69+
271 files**, `tsc --noEmit`, and all twelve `check:*` gates, clean.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/rest": patch
4+
"@objectstack/platform-objects": patch
5+
---
6+
7+
fix(spec,rest): the metadata forms save what they show — form ↔ Zod reconciliation (#3786)
8+
9+
Every entry in `METADATA_FORM_REGISTRY` is a hand-written `defineForm` layout
10+
that names keys of a Zod schema it never imports: two descriptions of one key
11+
set, a comment asking the next author to keep them in step, and nothing that
12+
fails when they don't. #3786 asked for a sweep of that shape across the repo.
13+
**Four of the seventeen forms had already drifted, every one of them silently.**
14+
15+
The silence is the point. `ObjectSchema` / `FieldSchema` are deliberately not
16+
`.strict()`, so a key the schema does not declare parses clean and is stripped
17+
on the way to storage — the same ADR-0104 failure class the `field.zod.ts`
18+
prune tombstone already describes in prose. An admin toggled a switch in
19+
Studio, got no error, and the value never landed.
20+
21+
**What was broken, from an author's seat:**
22+
23+
- **Object → Capabilities.** The block bound to `capabilities`; the
24+
`ObjectSchema` key is `enable`. All seven toggles (Track history, Searchable,
25+
API enabled, Files, Feeds, Activities, Clone) saved nothing.
26+
- **Object → Fields.** The inline column grid offered 16 keys `FieldSchema` has
27+
never declared. `PII`, `Encrypted`, `Indexed`, `Immutable`, `Filterable`,
28+
`Placeholder`, `Validation`/`Error message` and `Starting number` were
29+
controls with no storage behind them at all; the rest named keys the schema
30+
had **renamed** and the form never followed:
31+
`referenceFilter``lookupFilters`, `cascadeDelete``deleteBehavior`
32+
(a three-way enum, not a boolean), `formula``expression`,
33+
`displayFormat``autonumberFormat`, and the flat `summaryType` /
34+
`summaryField` pair → the single `summaryOperations` object, which also
35+
restores the `object` key the flat pair had no slot for. Roll-ups authored in
36+
that grid saved nothing.
37+
- **Report → Advanced.** `aria` and `performance` were pruned from
38+
`ReportSchema` by #3496; the form kept rendering both.
39+
- **Hook / Action → Body.** `memoryMb` was unauthorable — named in
40+
`hook.form.ts`'s own doc comment, absent from the list beneath it.
41+
- **Page → Interface.** `interfaceConfig.sort` was unauthorable, so a page's
42+
default sort order could not be set in Studio at all.
43+
44+
**No authored metadata changes and nothing you can write is removed.** These
45+
were UI controls that never persisted; every corrected key is one `FieldSchema`
46+
/ `ObjectSchema` already accepted. Metadata authored in YAML/TS was always
47+
validated against the real schema and is unaffected. If you had been filling
48+
those Studio controls expecting them to stick, they now either work (the
49+
renamed five) or are gone rather than lying to you.
50+
51+
The metadata-form translation bundles are derived from the registry, so all
52+
four locales are regenerated. Worth naming what they contained: translated
53+
labels, in four languages, for switches that saved nothing — the drift had
54+
propagated into a generated artifact and been dutifully translated there.
55+
56+
**The mechanism.** `metadata-form-zod-reconciliation.test.ts` walks every
57+
registered form and reconciles it against `getMetadataTypeSchema()`. The two
58+
directions are deliberately asymmetric: **form-only** (a control whose value is
59+
discarded) is always a defect and cannot be excused, because no design wants
60+
one; **zod-only** is ledgerable with a reason, for a deprecated key held back
61+
from new authoring or a curated quick-add subset that defers to a fuller
62+
editor. Ledger entries are checked for non-vacuity and for still resolving on
63+
both sides, per the #4045 / #4040 discipline. Verified by mutation — re-adding
64+
a stripped key, dropping a covered key, and offering a ledgered omission each
65+
turn the gate red.
66+
67+
**New export: `TRANSLATABLE_METADATA_TYPES`** (`@objectstack/spec/system`), the
68+
set of metadata types whose labels `translateMetadataDocument` localizes,
69+
derived from its dispatch table rather than restated. `@objectstack/rest` had
70+
been carrying a hand-copied literal set under a "keep in sync with the type
71+
dispatch" comment; it now reads this instead. Registering a translator in spec
72+
reaches the REST boundary with nothing else to remember — the second list is
73+
deleted rather than checked, which is the better half of derive-or-gate.
74+
75+
Also corrected: `ActionAiCategorySchema`'s comment claimed it mirrored
76+
`ToolCategorySchema` in `ai/tool.zod` and told the next author to update both
77+
sides — but #3896 deleted `ToolCategorySchema` along with the inert
78+
`tool.category` key it typed. The instruction had been pointing at a source
79+
that no longer exists. The enum is canonical now and says so.

content/docs/plugins/packages.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,10 +344,10 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern
344344

345345
### @objectstack/plugin-dev
346346

347-
**Developer Tools Plugin**Development-time utilities.
347+
**Local Development Plugin**one-line local stack: wires ObjectQL, the in-memory driver, auth, security, the HTTP server, REST and the dispatcher, then fills any still-unclaimed core service slot with an in-memory dev implementation.
348348

349-
- **Features**: Metadata validation, schema introspection, debugging tools
350-
- **When to use**: Development and debugging
349+
- **Features**: Composes the real plugins above; registers dev implementations for unclaimed slots, each declaring what kind of fake it is (`__serviceInfo``degraded` when it really does the work in memory, `stub` when it fabricates). Slots that would fabricate an answer over HTTP get no implementation at all: `analytics` and the three `security.*` handles stay empty on purpose.
350+
- **When to use**: Local development only. **`init()` throws under `NODE_ENV=production`** — set `OS_ALLOW_DEV_PLUGIN=1` only if you deliberately want the dev slate under a production `NODE_ENV`.
351351
- **README**: [View README](https://github.com/objectstack-ai/objectstack/blob/main/packages/plugins/plugin-dev/README.md)
352352

353353
### @objectstack/plugin-approvals

0 commit comments

Comments
 (0)