Skip to content

Commit 89372b7

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14935-publicsharing-canonical-predicate
2 parents 4620deb + 53cbad9 commit 89372b7

84 files changed

Lines changed: 10934 additions & 603 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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
`os lint --strict` makes warning-severity findings fail the run, so an app can rely on the platform's warning-level rules as its gate instead of re-implementing them locally (#15935)
6+
7+
Only an `error` failed `os lint` before. `packages/lint` ships ≈250 authoring rules, 119 of them at `warning`, and a run with any number of warnings and no errors exited 0 — so an app that wanted one of those rules to gate its CI had to re-implement it locally at error level, or bolt a script onto the JSON output to promote a family by hand.
8+
9+
New public flag: **`os lint --strict`**. With it, a run with one or more `warning`-severity findings exits 1 exactly as an `error` does, and the console says why, naming the count and the flag:
10+
11+
```
12+
✗ 1 warning(s) fail this run under --strict (a warning is advisory without the flag)
13+
```
14+
15+
`suggestion`s stay advisory under both. ⛔ The default is unchanged: without the flag the same stack still exits 0, and no existing `os lint` expectation moves.
16+
17+
The `--json` face carries the verdict so a gate can read it without re-deriving it from the counts. Two keys, unconditionally present on every project-lint payload, flag or no flag:
18+
19+
```json
20+
{ "passed": false, "errors": 0, "warnings": 1, "suggestions": 0, "strict": true, "failing": 1 }
21+
```
22+
23+
`strict` says whether the flag was in effect; `failing` is the count the exit code was read from — `errors`, or `errors + warnings` under `--strict`; and `passed` is `failing === 0`, the same statement the exit code makes — so `--strict --json` on a warning-only stack reads `passed: false` beside exit 1, never `passed: true` next to a failing exit.
24+
25+
Not in this change: per-rule severity configuration, any change to a rule's severity, and `--eval` mode, which keeps its own pass bar (`--eval-min`).
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
---
4+
5+
The datasource admin routes derive the tenancy posture before resolving the caller
6+
7+
`requireDatasourceAdmin` resolved the request with `resolveAuthzContext({ ql, headers, getSession })` and supplied no `tenancyPosture`. Both posture-conditional API-key refusals are gated on the caller supplying one — `organization_required` and `organization_membership_ended` — so neither ran on this family, and an API key stamped with an organization its owner had left was admitted; the routes then gated it on `authz.systemPermissions` alone. Because this family gates on system capabilities rather than on organization-scoped rows, the consequence was an admitted principal rather than a cross-organization row read.
8+
9+
The posture is now read off the kernel's `tenancy` service and classified rather than swallowed: a service that was never registered stays quiet (`undefined` — the supported no-tenancy composition, unchanged behaviour), while one that was registered and failed to build raises `AuthzStoreUnavailableError` instead of degrading to "no posture". Patch rather than minor: no accept set widens, and a declared guard returns to enforced.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/runtime": minor
3+
"@objectstack/spec": minor
4+
---
5+
6+
`POST /packages/:id/duplicate` now refuses a source that is not a writable base, instead of answering `200` with an empty copy.
7+
8+
Duplicating a **running code package** answered `HTTP 200` with `{"success":false,"copiedCount":0,"failedCount":0,"copied":[],"failed":[]}` — and still created the target package record, leaving a real, listed, empty package behind. The source package had one object, four flows, views, dashboards and reports; none of it was copied, and nothing said why.
9+
10+
`copiedCount: 0` there was **by construction**, not a copy that failed. `duplicatePackage` clones the rows `sys_metadata` holds for the source, and a code package's metadata is delivered as code — it has no such rows — so the scan could never have found anything. A caller could not tell that from a base that really is empty, which is the ambiguity the platform already refuses to ship elsewhere: *a read that could not happen must not be reported as a read that found nothing.*
11+
12+
- **The refusal.** A code-loaded, platform- or marketplace-scoped source is now refused `422` with the new error code `DUPLICATE_SOURCE_NOT_A_BASE` (registered under `@objectstack/runtime`), naming the package and prescribing the remedy that exists for it — duplicate a base you own, or customise the code package in place with an ADR-0005 org overlay. The refusal runs **before** the protocol call, so the empty target record is no longer created; the writability verdict is the same `isWritablePackage` predicate the authoring and lifecycle gates already use.
13+
- **The read-only lifecycle refusal stops prescribing a dead end.** `WRITABLE_PACKAGE_REQUIRED` (from `DELETE /packages/:id` and `PATCH /packages/:id/disable`) used to tell callers to "duplicate this one into a writable base (`POST /packages/:id/duplicate`) and change that" — a route which, for exactly the packages that refusal fires on, cannot help. It now points at the ADR-0005 overlay instead.
14+
15+
⚠️ Behaviour change for API callers: duplicating a code, platform or marketplace package was `200`, and is now `422`. Duplicating a **writable base** is untouched in every respect — including a base that owns no active rows, which still answers `200` with `copiedCount: 0`, because that read happened and found nothing.
16+
17+
Not changed: duplicate still does not clone a code package's items. ADR-0070 D4 duplicates a *base*, and is itself declared-and-not-built; teaching it to fork code packages would extend the decision rather than implement it, and the ADR still carries that as an open question.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os lint` / `os i18n check` stop reporting a written inline locale map as an untranslated string.
6+
7+
`I18nLabelSchema` authorizes two forms of a display label: a plain string, whose translations live in a bundle, and an **inline locale map**`{ en: 'Members', 'zh-CN': '成员' }` — written out at the authoring site and picked at render time. Rulings on both forms make the map the one localisation route for props that have no bundle key at all, so a page localised that way is fully localised.
8+
9+
The coverage walk could not see it. `inlineText()` narrowed a map to `undefined` — the same value an **absent** prop produces — so one diagnostic carried two opposite facts, and the gate reported a prop written out in four languages exactly as it reports a prop nobody wrote:
10+
11+
- with no bundle entry, the key was dropped from the expected set entirely: neither covered nor missing, invisible in the counts;
12+
- with a bundle entry for one locale, the key came back with no inline evidence, and every locale the **map** held and the bundle did not was reported `missing translation` — about text that was right there in the file.
13+
14+
An entry now carries a third axis beside `sourceValue` and `inline`: `inlineLocales`, the map the author wrote, verbatim. Coverage reads it per locale — a locale the map carries counts as covered, a locale it omits is reported as a gap, and the default locale is satisfied by the map the way it has always been satisfied by an inline string. The read is deliberately narrower than the renderer's: only the tag-matching limbs of the shared `resolveI18nLabel` rule count, because falling back to `en` or to the untagged `default` entry **is** what an untranslated locale looks like.
15+
16+
Two things this deliberately does not do. The map is still **never extracted**: no bundle row is scaffolded for it, and no key family is added — a translator working from the locale bundle still will not find these strings, which is the cost of the form and is now stated where an author chooses it (`i18n.zod.ts`, and the extractor's own header). And no key is synthesised from a node's position in the page tree: position-addressed keys would turn a reorder of two sibling components into a silent, all-green swap of their translations. If inline maps are ever to be extracted, the recorded direction is identity first — `component.id` / `section.name` / `tabs item.value` made mandatory and gate-enforced, then the existing `pages.<page>.components.<id>.<key>` family reused.
17+
18+
Net effect on a project that authors no inline maps: none. On one that does, the gate starts telling the truth in both directions — the false `missing translation` goes, and a map that genuinely omits a locale is reported for the first time.
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+
---
4+
5+
`getMetaItemLayered` no longer reports a phantom org-scoped row as a tenant customization.
6+
7+
`getMetaItemLayered` is the three-layer diagnostic behind Studio's "Code default vs Overlay vs Effective" view, and the third `/meta` read verb in the series `getMetaItems` (plural) and `getMetaItem` (singular) were repaired in. Unlike those two it applied no registry read gate of its own: whatever organization a caller passed was spent on whatever type it passed. On a type the registry declares `allowOrgOverride: false` — everything outside the ADR-0005 tier-A five (`view`, `dashboard`, `report`, `translation`, `email_template`) — a deployment with history can hold pre-#6190 phantom org-scoped rows, which boot hydration deliberately walks past. Read back through this verb they surfaced as `overlay` with `overlayScope: 'org'`: an operator was shown a customization that does not exist, in the one surface built to be authoritative about customizations.
8+
9+
It was not only displayed. Two doors return that layer **as the response** when it is non-null — the runtime metadata dispatcher and REST `GET /meta/:type/:name/published` — so on those paths the phantom was served as the item.
10+
11+
The read now resolves its organization through `organizationIdForMetaRead`, the same registry-derived predicate the REST `/meta` doors have applied since #9454 and the twin of the write side's `organizationIdForMetaWrite`. A type with a per-org read channel still resolves the caller's organization and still reports `overlayScope: 'org'`; every other type reads env-wide, which is the partition that actually runs.
12+
13+
**The gate is bound after the canonical type fold, and that ordering is load-bearing.** In the two sibling verbs the binding already sat below `canonicalizeMetaRequestType`, so the fix there was a substitution. Here it sat above it, and dropping the same expression in place would have gated on the raw `/meta/:type` segment: `declaresOrgOverride` tolerates the manifest plurals but not the URL-only spellings (`translations` and `email_templates` have no manifest key), so a raw segment splits one item across two partitions, addressed by spelling. The repair is therefore a reorder, and it is pinned by a test that fails if the binding moves back above the fold.
14+
15+
Callers that name no organization — four of the five `plugin-security` invocations, and every import/analytics/auth reader — are unaffected, and a door that already computed the same predicate receives the scope it did before.

.changeset/lucky-poems-invite.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/plugin-hono-server": minor
3+
---
4+
5+
fix(plugin-hono-server): the current-user faces assemble their `ExecutionContext` through the shared assembler (#15747)
6+
7+
**BREAKING** for TypeScript consumers — a published TYPE-surface narrowing, shipped as `minor` under the launch-window convention (`major` is refused by `check-changeset-no-major`, so the BREAKING banner and the ADR-0087 disposition are the carriers, not the level).
8+
9+
`makeExecutionContextResolver` is exported from this package's index. Its declared return moves **from** `(ctx: CurrentUserEndpointsContext) => (c: any) => Promise<any | undefined>` — in practice `any`, since the exported function carried no return annotation at all and the envelope it built was a hand-rolled object literal cast `as any`**to** `(ctx: CurrentUserEndpointsContext) => (c: any) => Promise<ExecutionContext | undefined>`. `any` is assignable to everything and admits every property read, so a consumer's code really can stop compiling.
10+
11+
What this asks of a consumer holding the resolver directly (the serverless host path that composes it, cloud#924): narrow the `undefined` arm before reading the envelope — under `strictNullChecks` the resolver has always been able to answer `undefined` for a request with no session, and no caller was ever asked to handle it; and stop reading members `ExecutionContext` does not declare, since the receiver is no longer `any`. A consumer that only calls `registerCurrentUserEndpoints` sees no change.
12+
13+
The envelope itself is now assembled by `assembleExecutionContext` (`@objectstack/core`) — the fail-closed entry every other HTTP transport already uses — instead of the hand-rolled literal, which omitted six fields of the closed entry set: `principalKind`, `onBehalfOf`, `audience`, `accessToken`, `authGate` and `oauthScopes`. `principalKind` is `'human'` on these faces, the value the shared assembler derives for a session-backed principal; the other five are withheld on the record. A field added to `ExecutionContext` from now on fails to compile here until this face decides it.
14+
15+
No runtime behaviour changes: `/auth/me/permissions`, `/auth/me/localization` and `/me/apps` answer byte-identical bodies, pinned as goldens.
16+
17+
<!-- adr-0087: not-required (type-surface-only packages/plugins/plugin-hono-server/src/current-user-endpoints.ts#makeExecutionContextResolver) A published return type moves off an erased `any` onto the kernel's own `ExecutionContext`: no metadata key is removed, renamed or re-shaped, this diff touches no `packages/spec/**` path and no ADR-0087 shape surface (no `*.zod.ts`, no `packages/spec/src/contracts/**` entry, no object definition), and nothing exists for `objectstack migrate meta` to rewrite. The affected party is a TypeScript consumer and the delivery channel is the compiler at their own call site, which reaches every one of them rather than the subset who read release notes. -->
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
The REST server's `api` configuration defaults now come from `RestApiConfigSchema` alone, instead of being restated in `packages/rest`.
6+
7+
`RestServer.normalizeConfig` already parsed `config.api` against `RestApiConfigSchema` — and then discarded the result, rebuilding the block from a `??` chain over the raw input. That chain restated the schema's eleven top-level `z.default(...)`s as eleven literals in a second package. They agreed key for key, and nothing measured that they would keep agreeing: changing a default in `@objectstack/spec` silently failed to propagate, because `api.enableUi ?? true` answers `true` for an absent key whatever the schema declares. Consuming the parse deletes the duplicate and makes the schema authoritative.
8+
9+
The parse itself is unchanged, so **nothing new is accepted or refused**: the same schema, with the same `.omit({ requireAuth: true })`, already ran at construction. `api.requireAuth` keeps its retired warn-and-ignore posture (`@objectstack/rest`'s plugin reads it off the raw config, so the warning is untouched), and every authored value still wins over the default.
10+
11+
One bounded behaviour change, for a caller who writes `api.documentation` or `api.responseFormat` — and it runs in two directions, not one. **Filled in:** those objects now arrive carrying their own declared inner defaults — `documentation.enabled` / `.title`, and `responseFormat.envelope` / `.includeMetadata` / `.includePagination`. **Stripped:** inner keys the schema does not declare no longer survive, at either depth — an authored `documentation.logo`, a `documentation.contact.phone` or a `documentation.license.spdxId` inside the nested objects, a `responseFormat.extra` — where the `??` chain passed the authored object through by reference and kept every key on it. Both halves are the same parse: `documentation` / `responseFormat` (and their `contact` / `license`) are non-strict `z.object()`s, which fill in their `.default()`s and drop what they do not name — dropped silently, so this is a strip and not a new refusal. An object left unwritten stays absent, and nothing in the platform reads either key today: the normalized block is `private` to `RestServer`, which reads only scalars off it (`apiPath` / `basePath` / `version` in `getApiBasePath`, the `enable*` flags, `projectResolution`), and the repo has no other read site for either key — so no consumer observes either half.

0 commit comments

Comments
 (0)