Skip to content

Commit 4d19901

Browse files
authored
Merge branch 'main' into claude/issue-11958-vitest-inner-pool-bound
2 parents a3dc841 + 494279c commit 4d19901

84 files changed

Lines changed: 3986 additions & 142 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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
fix(auth): authorize before the break-glass guard on `POST /api/v1/auth/admin/remove-user` (#11477)
6+
7+
The break-glass last-local-credential guard is registered as a global better-auth
8+
`hooks.before`, which runs ahead of an endpoint's own middleware. On
9+
`/admin/remove-user` — served directly by better-auth's router, whose
10+
`adminMiddleware` establishes only a session, with the role decision landing
11+
later inside the vendor's handler — that ordering let the guard's lookup and its
12+
distinctive refusal be reached by any **authenticated** caller before either
13+
authorization layer had run. Because that refusal is target-dependent, the
14+
refusal itself carried a per-record fact about a user the caller was not
15+
entitled to ask about.
16+
17+
`/admin/ban-user` already ran the same guard **after** authorization: #9652
18+
shades that path with an ObjectStack raw mount whose platform-admin gate fires
19+
first. One guard, two routes, opposite orders, and nothing asserting either.
20+
21+
`/admin/remove-user` now carries the same shading, converging the whole
22+
`/admin/*` family on **authorization before the guard**. The mount reuses the
23+
landed #9652 / #9653 pattern and introduces no new mechanism.
24+
25+
What changes is **when** the guard decides, never **what** it decides:
26+
27+
- an anonymous caller still gets `401 UNAUTHENTICATED`;
28+
- an authenticated non-admin now gets `403 PERMISSION_DENIED` for every target,
29+
so the guard is unreachable before authorization and its answer no longer
30+
varies with the named user;
31+
- a platform admin is unaffected in every respect — the mount **delegates** into
32+
better-auth rather than re-implementing removal, so the path-keyed hook still
33+
fires and the guard still refuses the removal of the last local password
34+
login, and admission remains the vendor's own decision (#9969).
35+
36+
An ordering pin ships with the fix so the sequence is mechanically checkable
37+
rather than re-argued: it asserts that one authenticated non-admin naming two
38+
different targets receives **indistinguishable** responses, and — so the pin
39+
cannot be satisfied by deleting the guard — that an admitted platform admin
40+
still hits the guard's refusal, and still succeeds on an ordinary user.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
feat(cli): `os build --json` carries the computed advisory lists on every failure exit, not the success payload alone (#11772)
6+
7+
**Machine-contract widening on the `--json` failure payloads.** A consumer that
8+
today branches on `warnings` being ABSENT from an `os build --json` failure
9+
payload sees a different shape after this change.
10+
11+
## What was wrong
12+
13+
The text face prints its advisory blocks before the gates that can stop the
14+
run — the #11529 author-time advisories at step 3b, the #3786 undeclared
15+
authoring-key findings at 3d — and both end in `— re-run with --json for the
16+
full list`. But `warnings` lived on the TERMINAL SUCCESS payload only (plus,
17+
for `ruleAdvisories` alone, the author-time-rules failure). On a tree with 60
18+
undeclared authoring keys *and* a package-docs error:
19+
20+
```
21+
os build Undeclared authoring keys (60) … 50 rows …
22+
… and 10 more … — re-run with --json for the full list
23+
os build --json {"success":false,"error":"docs validation failed","issues":[…]}
24+
^ the 60 keys nowhere
25+
```
26+
27+
The remedy the notice named returned a payload that did not contain the list,
28+
and the author could not reach the withheld entries by any route until an
29+
unrelated later failure was fixed — the "the remedy named is unreachable"
30+
shape of #11643 and #11391.
31+
32+
## Which exits gain the field
33+
34+
All nine failure exits of `os build --json`. Six already had a payload of their
35+
own; three more were found while enumerating (the filing card's table listed
36+
six). `warnings` is now present on every one, alongside each exit's existing
37+
keys, which are unchanged:
38+
39+
| exit (step) | existing keys | `warnings` before | after |
40+
| --- | --- | --- | --- |
41+
| `strict-body: missing body` (2b) | `issues` | absent | `[]` |
42+
| protocol parse failure (3) | `errors` | absent | `[]` |
43+
| `author-time rules failed` (3b) | `issues` | `ruleAdvisories` | unchanged |
44+
| `capability provider preflight failed` (3c) | `issues` | absent | rule + capability |
45+
| `access matrix drift` (3e) | `changes` | absent | rule + key + capability |
46+
| `docs validation failed` (3f) | `issues` | absent | all four lists |
47+
| `--no-runtime-bundle` refusal (4b) | `error` | absent | all four lists |
48+
| `runtime bundle failed` (4b) | `error` | absent | all four lists |
49+
| thrown / caught (bottom) | `error` | absent | what the run had computed |
50+
51+
The success payload is unchanged in content: its
52+
`[...ruleAdvisories, ...docWarnings, ...unknownKeyWarnings, ...capProviderWarnings]`
53+
spread — `os validate --json`'s order minus its trailing `structuralWarnings`
54+
— moved to a single `warningsSoFar()` site that every exit now reads, so the
55+
member order cannot drift between exits.
56+
57+
## What a consumer keying off its absence should do instead
58+
59+
`warnings` is no longer a signal of which exit produced the payload. Read
60+
`success` (and `error` / `errors`) for that; a consumer that inferred "this is
61+
a failure payload" from a missing `warnings` must switch to `success === false`.
62+
63+
`warnings: []` on a failure payload does NOT mean "this tree raises no
64+
advisories". It means **this run stopped before those advisories were
65+
computed** — the two early exits above (`strict-body`, protocol parse) run
66+
before any advisory step, so their list is empty by construction. A consumer
67+
that needs the full advisory set for a tree must read it from a run that
68+
reaches at least the gate that computes it, or from `os validate --json`.
69+
70+
`warnings` is always an array on every `os build --json` payload, success or
71+
failure, so it can be read unconditionally — that shape constancy is the point
72+
of the change (maintainer ruling 2026-08-25, option 1 of three; option 2,
73+
"carry them only where the text face printed them", was rejected as the hardest
74+
contract to declare).
75+
76+
Advisories stay CARRIED, never recomputed: each list is still computed at
77+
exactly the step that owns it, so an exit upstream of a step legitimately
78+
reports that list empty and no failure path pays for a computation it did not
79+
already do.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
"@objectstack/client": minor
3+
---
4+
5+
fix(client): bind the three verifiable methods of the unannotated return-type erasure population to their spec contracts (#11925)
6+
7+
**Return-type narrowing on a published SDK.** No runtime change — the value each
8+
method resolves to is byte-identical before and after. Only the DECLARED type
9+
moved, off `any`, which is precisely why a runtime test cannot observe it and
10+
the pins for it are type-level.
11+
12+
`any` is assignable to everything and admits every property read, so for each
13+
method below a consumer's code could stop compiling where it previously did
14+
not: assigning the result to an unrelated annotation, reading a property the
15+
bound type does not declare, or forwarding the value to a differently-typed
16+
parameter.
17+
18+
## What changed, per family
19+
20+
**`client.packages.list``{ packages: InstalledPackage[]; total: number }`**
21+
(was `{ packages: any[]; total: number }`). A consumer stops compiling if it
22+
reads any key off a row that `InstalledPackage` does not declare. Note in
23+
particular `source` (`'database' | 'registry' | 'both'`): the REST surface
24+
spreads it onto each row, the dispatcher surface does not, and it is therefore
25+
deliberately NOT declared — code reading `pkg.source` off this result compiles
26+
today and will not after. `total` and the array envelope are unchanged.
27+
28+
**`client.packages.update``InstalledPackage`** (was `any`). This method
29+
declared no envelope before, so nothing about the shape claim changed; a
30+
consumer stops compiling if it reads an undeclared key off the returned row, or
31+
assigns the result somewhere `InstalledPackage` does not fit.
32+
33+
**`ScopedProjectClient.packages.get``{ package: InstalledPackage }`** (was
34+
`{ package: any }`). The `{ package }` envelope is unchanged; only the member
35+
narrowed. A consumer stops compiling if it reads a key off `.package` that
36+
`InstalledPackage` does not declare — again including `source`, which this
37+
route does send and which stays undeclared for consistency with its already
38+
bound `list` sibling.
39+
40+
## What deliberately did NOT change
41+
42+
The other 36 methods in the measured population keep their erased `any`, each
43+
with a docblock stating why and pointing at the issue that carries it:
44+
`meta.*` history/diagnostics (9) and eight `packages.*` routes have no published
45+
response contract to bind to (#12038); `client.packages.get` has two mounted
46+
surfaces that emit different envelopes and `install`/`enable`/`disable` declare
47+
an envelope no surface emits (#12034); the 15 cloud `projects.*` methods call a
48+
control plane that speaks snake_case while the `@objectstack/spec/cloud` rows are
49+
camelCase, so binding to them would compile and be false (#12036).
50+
51+
No consumer loses anything by those staying `any` — they are exactly as
52+
permissive as before.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/metadata-core": minor
3+
"@objectstack/objectql": minor
4+
---
5+
6+
feat(metadata-core,objectql): publish `assertEngineFindOnePredicate` — the read-side member of the engine-double contract family (#11957)
7+
8+
`ObjectQL.findOne` applies `limit: 1`, so a query naming no particular record
9+
would return an ARBITRARY row. `requireFindOnePredicate` (#4419) REFUSES that
10+
call. Every in-memory test double in the repo instead read an absent filter as
11+
"match everything" and answered happily, so a production call site that violates
12+
#4419 read as *working* under every unit suite and only failed on a real engine.
13+
14+
That is measured, not hypothetical. `AuthManager.isBootstrapCreation` probed the
15+
bootstrap population with `findOne({ where: [] })` inside a `try/catch`; on a
16+
real engine that throws, the `catch` read the refusal as "users exist", and the
17+
declared first-run bypass became permanently inert on real deployments — while a
18+
641-line unit matrix over the double stayed green, including a case named
19+
"bootstrap: the very first signup is admitted" (#11767).
20+
21+
New public API, mirroring the two write-side dispatch predicates
22+
(`assertEngineDeleteDispatch`, `assertEngineUpdateDispatch`) exactly — the
23+
implementation lives in `@objectstack/metadata-core` so that packages
24+
`@objectstack/objectql` itself depends on can reach it, and `@objectstack/objectql`
25+
re-exports every symbol:
26+
27+
- `assertEngineFindOnePredicate(object, query)` — the line a fake engine's
28+
`findOne` opens with; throws the engine's own message, object name included.
29+
- `resolveEngineFindOnePredicate(object, query)` — the same decision without the
30+
throw, for a double that wants to classify.
31+
- `engineFindOnePredicateRefusalMessage(object)` — the refusal text, so an
32+
assertion pins the producer's wording rather than a paraphrase.
33+
- `ENGINE_FINDONE_PREDICATE_CASES` — the shared conformance case-set, driven
34+
against the REAL engine by
35+
`packages/objectql/src/engine-findone-predicate.test.ts`, so the predicate
36+
cannot drift from `engine.ts` unnoticed.
37+
38+
Nothing is removed and no existing behaviour changes: the engine's own guard is
39+
untouched, and this publishes the decision it already makes so a double can
40+
import it instead of re-deriving it.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): strip the internal issue-id references that were projected into the published skill catalog
6+
7+
The 2026-08-23 ruling stripped internal `#NNNN` citations from the published
8+
skill corpus, but 14 of them were not authored in `skills/**` at all — they were
9+
projected there from `.describe()` / TSDoc text in `packages/spec/src/**` by
10+
`gen:skill-refs` and `gen:react-blocks`, so a hand-edit of the corpus could not
11+
reach them and a regeneration would have put them straight back.
12+
13+
Six source sites are rewritten to say the same thing without the citation, and
14+
the artifacts are regenerated: the module summaries of `data/driver/common`,
15+
`data/driver/config-registry`, `data/driver/turso`, `shared/retry-policy` and
16+
`system/translation`, plus the `ListView.objectName` / `ListView.viewType`
17+
deprecation notes and the `<Block>` summary in `ui/react-blocks`. The teaching in
18+
each is kept, per the standing ruling of 2026-08-12, verbatim and untranslated:
19+
「处理 issue 时犯的错应该总结成经验,保留 issue id没有意义」.
20+
21+
Customer-facing text changes in three places from the one source edit: the
22+
published catalog (`skills/*/references/_index.md`,
23+
`skills/objectstack-ui/references/react-blocks.md` and its sibling
24+
`contracts/react-blocks.contract.json`), and the docs site
25+
(`content/docs/references/data/driver-common.mdx`, `driver-turso.mdx`). No
26+
schema shape, no `.describe()` used for validation, and no accept/reject
27+
behaviour changes — the edits are comment and documentation text only.
28+
29+
The doc-authoring gate's path exemption for the generated artifacts is removed
30+
in the same change: it existed only because those files still carried projected
31+
ids, and an exemption over a surface that no longer needs one is where the next
32+
regeneration would smuggle one back in.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/driver-sql": patch
3+
---
4+
5+
**Fix:** on SQLite, `applyMigrationEntries` no longer reports an op as **applied** just because a table rebuild ran (#11722).
6+
7+
`SqlDriver.applyMigrationEntries` splits by dialect, and the two arms disagreed about what `applied` means. The in-place arm (Postgres / MySQL) asks per entry and believes the answer — `applyDriftOpInPlace` returns `false` for an op its dialect cannot perform, and the entry goes to `skipped`. The SQLite arm did not ask at all: it called `rebuildSqliteTablePatched(table, ents)` and then `applied.push(...ents)`, every entry, unconditionally. But that rebuild honours exactly four op types — `relax_not_null`, `tighten_not_null`, `drop_column`, `drop_column_default` — and silently ignores everything else; its own docblock already said so for the varchar ops. An ignored op was still reported applied.
8+
9+
**The failure mode is a false green, not an error.** Nothing throws and nothing is skipped, so every consumer announces work that never happened: `reconcileAndWarnDrift` logs `auto-reconciled <op> on <table>`, and the artifact boot gate prints `↪ migrated <op>`. The finding is still physically present, so the next boot detects it again, reports drift again, and "migrates" it again — a loop with no failing signal anywhere in it.
10+
11+
**What changes.** `rebuildSqliteTablePatched` now returns the entries it actually acted on, built in the same pass that fills the four column sets it already partitioned into — deliberately not a second list of op types to keep in sync, so the returned set cannot drift from the work done. The caller reports those as `applied` and routes the remainder to `skipped`, logging it in the **same sentence** the in-place arm uses for an op its dialect cannot do (`<op> on <table>.<column> is unsupported on dialect 'sqlite' — skipped`), so one greppable line covers all three dialects. `@objectstack/driver-sqlite-wasm` and `@objectstack/driver-turso` extend `SqlDriver` without overriding either method, so both inherit the correction.
12+
13+
**What deliberately does not change.** No op does anything different — this moves only what is *reported*. In particular the rebuild still runs for the whole table even when it honours nothing: it re-materializes every kept column's default (#11321, #4560) and the full declared index set from metadata (#3696), so it is not a no-op, and suppressing it would change what the reconciler DOES rather than what it says it did. `applied`/`skipped` remains a reported partition consumed by log lines and CLI counts; it is not an accept/reject door, and no public surface widens.
14+
15+
**Latent when found, and fixed anyway.** The gap was unreachable at the time of the fix, held closed from two independent directions neither of which knew it was holding it: `enforcesVarcharLength` excludes SQLite, so the differ never emits `widen_varchar`/`narrow_varchar` there, and `multiValueColumnTypeIsLoadBearing` excludes SQLite for an unrelated measured reason, so #11535's `manual_column_type_change` is never emitted there either. The next column op that is not SQLite-rebuildable would have opened it silently. `manual_column_type_change`'s own docblock states that `applyMigrationEntries` reports it "skipped, never applied" — measured on Postgres and MySQL; that sentence is now also true on SQLite, and the docblock says so.
16+
17+
Pinned by `packages/drivers/driver-sql/src/sql-driver-11722-sqlite-rebuild-applied-honesty.test.ts`, which constructs the reachability rather than waiting for it — it substitutes only the differ's dialect guard, handing entries straight to the public `applyMigrationEntries` seam that `os migrate apply` and the artifact boot gate both call, with a real driver, dialect and database throughout. All five cases fail on the pre-fix tree, including the consumer-level one that catches `auto-reconciled` being logged for an op that never happened.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@objectstack/platform-objects": patch
3+
"@objectstack/plugin-webhooks": patch
4+
"@objectstack/service-messaging": patch
5+
---
6+
7+
fix(i18n): re-translate the five leaves that served a superseded source revision (#12065)
8+
9+
`os i18n extract` merges gaps only, so a revised source string leaves the previous
10+
revision standing in every translated locale — in sync by key, green under
11+
`check:i18n` and counted as translated by `check:i18n-coverage`. The five leaves
12+
`check:i18n-stale-fill` froze in its baseline are re-translated here from the
13+
**current** `en` source, and the baseline is ratcheted to empty in the same change.
14+
15+
User-visible admin/Setup help text changes in `es-ES`, `ja-JP` and `zh-CN`:
16+
17+
- `dataset.fields.measures.helpText` (metadata forms) — all three locales promised a
18+
`"certified"` governance flag that was removed from the declaration in 16.0.
19+
- `sys_webhook.fields.method.help` — all three locales served the pre-revision method
20+
enumeration after the source became a prose description.
21+
- `sys_webhook.pluralLabel``ja-JP` was an untranslated Latin fill and is now
22+
Japanese; `zh-CN` keeps `Webhook`, which is the term this bundle's own Chinese prose
23+
uses and which carries no plural inflection.
24+
- `sys_http_delivery.fields.attempts.help``es-ES` / `ja-JP` held an English fill and
25+
`zh-CN` a translation of the same superseded source; all three now carry the
26+
PARKED / terminal-row clause the source documents.
27+
- `sys_notification_subscription.fields.principal.help` — the selector list was missing
28+
the `owner_of:object:id` and bare-email forms in all three locales.
29+
30+
No schema, export or runtime behaviour changes: translated-locale leaf values only,
31+
plus the shrink-only ratchet baseline.

0 commit comments

Comments
 (0)