Skip to content

Commit 689e8f6

Browse files
committed
Merge origin/main into claude/issue-23072-s2-dispatcher-read-decision
Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
2 parents af356c3 + d4aaeaf commit 689e8f6

35 files changed

Lines changed: 1832 additions & 174 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/lint': major
3+
---
4+
5+
A retired page-component prop is refused at `objectstack validate`, `objectstack build` and `objectstack lint`. Before this change, a key that `ComponentPropsMap` tombstones with `retiredKey()` (`object-grid` `defaultFilters`, `element:record_picker` `displayField`, `page:header` `breadcrumb` and the rest) drew only a `component-props-invalid` warning. The command printed "✓ Validation passed" and built the artifact, while the conversion registry told authors the key is refused. It is now a `component-props-invalid` error carrying the tombstone's prescription, and the stack does not pass until the key is gone.
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (already-registered object-grid-default-filters-removed, object-grid-default-sort-removed, object-grid-resizable-columns-removed, object-kanban-quick-add-removed, page-header-breadcrumb-removed, page-structure-inert-keys-removed, page-card-body-to-children, page-tabs-type-to-tab-style, record-details-layout-removed, record-picker-display-field-to-label-field, record-picker-inert-keys-removed, element-input-target-variable-removed, element-flat-data-binding-to-data-source, element-filter-removed, element-form-removed, object-master-detail-form-detail-sort-field-removed, list-view-navigation-view-retired, form-field-public-picker-retired, chart-config-aria-retired, ui-ai-chat-window-retired) Every key this refuses was retired, with its tombstone prescription and its ledger entry, before this change. This diff moves only the severity of the lint finding that carries the prescription, so it adds no entry and amends none. -->
10+
11+
**BREAKING**: an accept-set narrowing on a published authoring surface (the `component-props-invalid` rule of `objectstack validate`, `objectstack build` and `objectstack lint`).
12+
13+
**What is refused.** A value verdict a tombstone raises: the tombstone's own issue, wherever the key sits in the props bag, an array entry's key included. A tombstoned key inside a union member is refused when that member is the only one judging keys (`object-form` `sections[].fields[]` `publicPicker`). A retired element's props row (`user:profile`, `ai:chat_window`) raises the same issue and is refused the same way.
14+
15+
**What stays a warning.** Every other `component-props-invalid` finding: a wrong type, a missing required prop, a closed-set refusal (a url action's `target` scheme, a `record:details` section's `count`), a retired enum value and a missing `dataSource.object` binding. Every `component-props-unknown-key` finding also stays a warning.
16+
17+
**Not judged here.** The runtime publish door (the Studio, REST `/meta`, MCP) does not run this rule, so a page saved there is not refused for a retired prop. `defineStack` and the stack schema keep the open `properties` bag as written.
18+
19+
## FROM → TO
20+
21+
| you wrote | write instead |
22+
|:--|:--|
23+
| `object-grid` `properties: { defaultFilters: [...] }` with an empty `filter` | `properties: { filter: [...] }` |
24+
| `element:record_picker` `properties: { displayField: 'title' }` | `properties: { labelField: 'title' }` |
25+
| a retired prop whose prescription says to delete it, e.g. `page:header` `breadcrumb` | nothing: delete the key |
26+
27+
**The one-line fix: apply the prescription the error prints.** Where an ADR-0087 conversion covers the key, `objectstack migrate meta --from <N>` lists the mechanical edits for existing sources.
28+
29+
**Who is affected, measured.** At objectstack `21c86444e`, this rule ran over `examples/app-crm`, `app-todo`, `app-multi-package` and `app-showcase` (through `objectstack validate`), over the five shipped pages outside them (`@objectstack/platform-objects` ×3, `@objectstack/plugin-approvals`, `packages/qa/downstream-contract`) and over the `create-objectstack` template, which authors no page. Result: 0 newly refused, and 0 findings of this rule of any kind. Deployed metadata and other repositories were not measured.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
---
4+
5+
fix(metadata-protocol)!: the metadata save door never sets, changes or removes an item's lock: a body's `_lock*` family that would do so is refused with a located `400 VALIDATION_ERROR`, an echo of the served lock is accepted and ignored, and the row keeps the lock it stores (ADR-0010 §3.5, §4.1)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: the `_lock*` keys stay declared on every type that declares them, a code package's `protection` block keeps translating into them at load, and no spec key, authorable spelling, export or stored shape is added, removed, renamed or re-shaped. No stored row is rewritten or unlocked: a row keeps its bytes and loads unchanged, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is one runtime write door: a body saved through the metadata API whose `_lock*` family would set, change or remove the item's lock is refused, and the remedy is in the request a caller sends to that door, never a rewrite of a package source or a stored row. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and no exported declaration is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
10+
11+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
12+
13+
The `_lock` family (`_lock`, `_lockReason`, `_lockDocsUrl`, `_lockSource`) is the runtime's private lock envelope. A code package's loader stamps it from the package's `protection` block, and an install or a migration's re-save carries a stored one. ADR-0010 strips it on write and reattaches it on load (§4.1), and an item saved through the metadata API is authored in the environment, which takes no package lock (§3.5).
14+
15+
**This entry supersedes one sentence of the entry `23096-save-door-protection`, in this same release.** That entry says, under its unchanged behaviour, "A body without `protection` saves as before". What holds now: a body without `protection` saves as before only when it also sends no `_lock*` key, or sends back exactly the `_lock*` family a read of that item serves; a body whose `_lock*` family would set, change or remove the item's lock answers `400 VALIDATION_ERROR` and writes nothing, as this entry states. Where the two entries disagree, this one is current.
16+
17+
**FROM.** `PUT /api/v1/meta/{type}/{name}` stored a body's `_lock*` family verbatim. A hand-written `{ "_lock": "full" }`, or one an author or an AI copied from another item's served body, answered `200`, and the item then read `lock: 'full'`, `editable: false`, `deletable: false`: its next save and its delete both answered `403 ITEM_LOCKED`, and no door unlocks it. Any holder of the authoring capability could make an environment item permanently uneditable and undeletable. The other way round, an author save that sent no `_lock*` key dropped a lock the platform had stamped on the row, so a `no-delete` stopped refusing the delete.
18+
19+
**TO.** An author's save keeps the lock family the row it writes already stores.
20+
21+
- **No `_lock*` key in the body.** The save goes through, and the row keeps its stored lock. A body that says nothing about the lock does not remove it.
22+
- **An echo is accepted and ignored.** Studio and the AI authoring surfaces send the served body back on every edit, so a family equal to one a read of the item serves passes: the family the item read, the layered read's `effective` and the list serve (the item's resolved lock), or the family stored on the row the save writes (what the draft read and the layered read's `overlay` layer serve). The row still keeps its own stored family, so an echo of a packaged item's lock on its first overlay stores none in the environment row; the package's lock still binds.
23+
- **Anything else is refused**, `400 VALIDATION_ERROR`, and nothing is written: a family that would set a lock (on a new item, or on one that has none), change one (its state or its prose) or remove one (`_lock: 'none'`, or a partial echo that drops part of it), and a family copied from another item. The message names each `_lock*` key sent and the door that does lock an item, and `issues[]` carries each key as a path, so the Studio designer can highlight it.
24+
- **Every authoring write**: the REST and dispatcher saves, draft and publish, the external-table import, the unmanaged install, and the domain doors that save through them (the flow and permission-set clones, the permission-set and position write-throughs, the package publish's app flip). A type with no schema (`external_catalog`) is judged too.
25+
- **Unchanged.** A `_lock*` value the type's declaration refuses (`_lock: 'locked'`, a non-string reason) is still the schema's `422 INVALID_METADATA`, so the unmanaged install's refusal of such an item does not move. The server-side rewrites (`os migrate meta --stored --apply`, the stored flow credential move, package duplication) keep the family their body carries. A packaged item's lock still binds, and a save against a locked item still answers `403 ITEM_LOCKED` first. No lock stored today is released.
26+
- **Remedy.** Delete the `_lock*` keys from the body you send to the metadata API, or send them back exactly as a read of that item served them. To lock an item, declare `protection` on it in a code package's source, where the package loader translates it into the item's lock.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime): cloning a flow, and publishing a package's drafts, no longer fail on a `protection` block left in a row stored before the metadata save door refused it
6+
7+
Clause-②: no
8+
9+
Two runtime doors re-save a body the server builds from a stored row: the flow clone (`POST /api/v1/automation/:name/clone`), and the visibility flip of `POST /api/v1/packages/:id/publish-drafts`, which re-saves each of the package's apps still stored with `_unpublished: true`. The metadata save door now refuses a `protection` block with `400 VALIDATION_ERROR` (entry `23096-save-door-protection`). A row stored before that refusal can still carry a block, and both doors copied it into the body they saved.
10+
11+
**FROM.** A clone of such a flow answered `400 VALIDATION_ERROR` about a block the admin never sent. The flip of such an app was refused the same way: the publish still answered `200`, the app stayed `_unpublished`, and the refusal was reported only under `unhideError`.
12+
13+
**TO.** Both doors drop the `protection` block from the body they save, as the permission-set clone already does. The clone and the flip save, and the saved row carries no block. Nothing else in either body changes, and neither door states a `source`, so every other save-door check still applies. The block never bound anything: only a code package's loader translates `protection` into a lock, so dropping it changes no lock and no verdict.
14+
15+
**Unchanged.** A flow or an app with no block is saved exactly as before.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/service-storage': patch
3+
---
4+
5+
fix(service-storage): a presigned token the local adapter refuses answers `401 INVALID_TOKEN` on both raw byte routes, never a `500`
6+
7+
Clause-②: no
8+
9+
`PUT` and `GET /api/v1/storage/_local/raw/:token` are the local adapter's presigned byte routes. Each now answers every token it will not honour with `401 INVALID_TOKEN`, the status, code and message (`Invalid or expired token`) the error catalog already published for this code. The four cases are a malformed token, a forged one, one minted for the other verb, and an expired one.
10+
11+
- **Before:** a malformed token, or one minted for the other verb, answered `500 INTERNAL`, which reads as a server fault. A forged or an expired token answered `403`, with a message naming which check failed. A genuine fault whose text happened to contain "expired" or "signature" was answered as a refused token.
12+
- **Now:** every refusal gets the same status, code and message, so the answer never tells a forger whether the signature or the expiry failed. Any other failure behind a valid token is still `500 INTERNAL`, whatever its text says.
13+
- **A 5xx withholds its message text.** Per the platform rule, a 5xx on the raw byte routes answers with the route's generic text and not the fault's own; the original error stays in the server log.
14+
- **Token signatures are compared in constant time.**
15+
- **Unchanged:** a valid token's upload or download, the `501 NOT_IMPLEMENTED` answer when no adapter can verify tokens, and every other storage route.
16+
17+
What changes for you: a stale or tampered signed URL now answers `401` where it answered `403` or `500`. To recover, request a fresh URL (`GET /api/v1/storage/files/:fileId/url`, or a new presigned upload). The SDK's `storage.upload` and the console's upload adapter only check that the response is OK, so neither needs a change.

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ orthogonal to both, and no cell here can carry it; it is written out in
470470
| Chart bindings outside dashboards (#3583) | ✓ | ✓ | ✓ | — |
471471
| Navigation vs. granted access (ADR-0090 D6) | ✓ | ✓ | ✓ | — |
472472
| SDUI scoped styling (ADR-0065) | ✓ | ✓ | ✓ | — |
473+
| Retired SDUI component props — a key `ComponentPropsMap` tombstones (`retiredKey()`), refused with its prescription (#23026) | ✓ | ✓ | ✓ | — |
473474
| JSX / React page source parses (ADR-0080/0081) | ✓ | ✓ | ✓ | — |
474475
| Approval-node approvers (ADR-0090 D3) | ✓ | ✓ | ✓ | ✓ᶠ |
475476
| Security posture (ADR-0090 — e.g. every custom object declares `sharingModel`) | ✓ | ✓ | ✓ | ✓ˢᵖᵉᵇᵒ |
@@ -484,7 +485,7 @@ orthogonal to both, and no cell here can carry it; it is written out in
484485
| Flow authoring anti-patterns (#1874) | ✓ | ✓ | ✓ | ✓ᶠ |
485486
| Flow trigger readiness — a flow that looks armed and never launches (#5762) | ✓ | ✓ | ✓ | ✓ᶠ |
486487
| `views[]` conditional-visibility predicates — CEL syntax, parse budget, bare identifiers, binding-root layer, schema path refs (ADR-0089 D3b, #7010) | ✓ | ✓ | ✓ | ✓ᵛ |
487-
| Advisory: record titles, semantic field pointers (ADR-0085), form-section layout, action placement, SDUI component props, seed replay/state safety, capability references, liveness | ✓ | ✓ | ✓ | — |
488+
| Advisory: record titles, semantic field pointers (ADR-0085), form-section layout, action placement, SDUI component props (every finding but a retired prop), seed replay/state safety, capability references, liveness | ✓ | ✓ | ✓ | — |
488489
| Package docs — flatness, prefixes, links (ADR-0046) | ✓ | ✓ | ✓ | — |
489490
| Undeclared authoring keys — every metadata collection (#3786) and the stack's own top-level keys (#4167) | ✓ | ✓ | — | — |
490491
| Declared-unique scope — a bare `unique: true` index, a field-level and index-level double declaration, legacy organization composites (ADR-0120 D5) | ✓ | ✓ | ✓ | — |

‎content/docs/protocol/objectui/layout-dsl.mdx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -739,9 +739,10 @@ properties:
739739
<Callout type="info">
740740
`properties` is an open bag (`Record<string, unknown>`), so **it is the one part
741741
of a component the spec parse does not check**. For the platform's own types the
742-
authoring rules dispatch `ComponentPropsMap` and reject a misspelled prop; a
743-
`custom.*` type has no entry there, so its props are validated only by whatever
744-
registered the component.
742+
authoring rules (`os validate`, `os build`, `os lint`) dispatch `ComponentPropsMap`:
743+
a misspelled prop is reported as a warning, and a retired prop is refused as an
744+
error, with its prescription. A `custom.*` type has no entry there, so its props
745+
are validated only by whatever registered the component.
745746
</Callout>
746747

747748
## Advanced Layouts

‎packages/lint/src/authoring-rules.ts‎

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,27 +1155,18 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
11551155
// while a schema error elsewhere in the stack cannot take these findings down
11561156
// with it.
11571157
//
1158-
// Advisory, deliberately, and this is the whole shape of #5068's first step:
1159-
// wiring the parse is the precondition for enforcement, not the enforcement
1160-
// (#5020, one surface over). The live corpus violates the declarations in two
1161-
// places that are open contract questions — inline i18n label maps on three
1162-
// published platform pages (#5728) and the record picker's declared-but-unread
1163-
// `displayField` (#5775) — so gating today would fail the platform's own pages
1164-
// to enforce declarations the platform does not keep. The error upgrade is a
1165-
// separate step, once the warning-period inventory is empty.
1166-
//
1167-
// #5775 settled the record picker's half: `displayField` is retired in favour
1168-
// of the `labelField` the renderer actually reads. Its claim that "the rest of
1169-
// the keys the renderers honour are declared" did NOT hold — #6776 found five
1170-
// more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
1171-
// `page:accordion.variant`, and the tab strip's visual style, whose declared
1172-
// spelling `page:tabs.type` collided with the component node's own dispatch
1173-
// key and so was unauthorable in the flat and JSX carriers). All five are
1174-
// declared as of #6776, the last as the renamed `tabStyle`. What remains
1175-
// before the error upgrade is #5728 and two page rewrites.
1158+
// Gating since #23026, for ONE class: a `retiredKey()` tombstone's verdict is
1159+
// an `error`, so `os validate`, `os build` and `os lint` refuse a retired
1160+
// prop with its prescription. Every other finding stays a `warning`. #5068's
1161+
// first step was advisory on purpose — wiring the parse is the precondition
1162+
// for enforcement, not the enforcement (#5020, one surface over) — and the
1163+
// error upgrade was to be its own step once the warning-period inventory was
1164+
// empty. Its blockers (#5728, #5775, #6776's undeclared props, two page
1165+
// rewrites) are all closed, and the inventory measured 0. The rule's module
1166+
// header records the measurement and why the other classes stay advisory.
11761167
{
11771168
name: 'validateComponentProps',
1178-
tier: 'advisory',
1169+
tier: 'gating',
11791170
input: 'normalized',
11801171
commands: ALL,
11811172
source: 'packages/lint/src/validate-component-props.ts',

0 commit comments

Comments
 (0)