Skip to content

Commit 08fd7b3

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-10427-scansource-nested-template
2 parents 7a5d8a0 + 5e5df6a commit 08fd7b3

36 files changed

Lines changed: 3537 additions & 202 deletions
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+
**Behaviour change (tightening) — `POST /datasources/:name/external/validate` now requires `manage_platform_settings`** (#10255, completing the #9901 federation-family gate). This was the one route of the external-datasource federation family still admitting **any authenticated caller**; it now requires the same capability as the family's two read routes. Maintainer ruling, 2026-08-20 (verbatim: 「同意你的意见。」, accepting option A on #10255).
6+
7+
**This is published SDK surface.** `datasources.external.validate` on `ObjectStackClient` and the CLI's `os datasource validate` reach exactly this route. An existing integration that presents a valid credential — a better-auth session or a `sys_api_key` — and does not hold `manage_platform_settings` was served before and is **refused now**: `403` with the standard catalog code `PERMISSION_DENIED` (ADR-0112), the message naming the missing capability so the caller knows which grant to request. The anonymous floor is unchanged: no identity is still `401 UNAUTHENTICATED`.
8+
9+
**Why the read capability.** `validateAll` drives the same live remote-schema introspection the family's gated read routes expose (`introspect` per datasource), and its report — schema diffs naming remote columns and types, driver error strings for unreachable remotes — is a read of the same federation surface. An unentitled caller refused at `GET /:name/external/tables` could previously still trigger live remote introspection through this route and read what it found. One family, one door-type: reads on `manage_platform_settings`, writes on `manage_metadata`.
10+
11+
**Migration.** Grant the calling credential's permission set `manage_platform_settings` — the same grant the family's read routes have required since #10254, so an integration already migrated for those is covered. The platform's `admin_full_access` set carries it; a purpose-built operator set is the case to check.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/rest": patch
3+
"@objectstack/metadata-core": patch
4+
---
5+
6+
**Fix:** the REST `/meta` doors now decide **organization scope on the folded type**, never on the raw URL spelling (#10340).
7+
8+
Storage folds `/meta/:type` through `META_URL_TO_SINGULAR` — the complete spelling map — while the doors' scope predicate (`declaresOrgOverride`) tolerates only the manifest-collection spellings. For the two registry-derived spellings, `translations` and `email_templates`, the doors therefore read and wrote **env-wide** where the singular twin was org-scoped: an org-active author's `PUT /meta/translations/:name` landed an env-wide row their own org-scoped read then shadowed (persisted, receipted as live, served by nothing), and `GET` under one spelling answered a different partition than the other — one item, two namespaces, addressed by spelling (#4432 / #7894's defect one layer down).
9+
10+
- All nine `/meta` org-scope call sites (list, single read, layers view, compound read, save, compound save, delete, publish, rollback) fold the segment through `canonicalMetaUrlType` **before** calling `organizationIdForMetaRead` / `organizationIdForMetaWrite`, exactly as `metadata-url-spelling.ts` mandates: folding happens at the boundary and only there.
11+
- The `GET /meta/:type/:name/published` code-store fallback folds too — the smaller second site of the same class: it reads a registry keyed by canonical types, so a recognised plural of a code-published item answered 404 while the singular answered 200.
12+
- **Deliberately unchanged:** `GET /meta/_drafts` still applies no fold (it filters by the draft row's *stored* type, which is canonical because the protocol folds on save), the request `type` handed to the protocol stays the raw segment (the protocol owns its own fold), and `declaresOrgOverride` does **not** absorb the URL map — a predicate below the boundary consuming the URL spelling contract is the repair #7894 forbids. `@objectstack/metadata-core` changes are documentation and pins only: the predicate's header no longer claims parity with the protocol's normalization (measured false), and new tests pin both the composed fold→predicate contract and the predicate's deliberate limit.
13+
14+
No stored rows move: rows previously minted env-wide through a plural spelling stay env-wide and keep serving org-less callers (and org-active callers until an org overlay exists), which is the same layering the singular spelling always had.

.github/workflows/lint.yml

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,32 @@ jobs:
172172
- name: scripts/ entry guards go through one predicate
173173
run: pnpm check:entry-guard
174174

175+
# Every `scripts/**` TypeScript parse goes through ONE module (#10133 /
176+
# #10573), and this is the half that keeps it that way (#10574).
177+
# NONE of the three parser entry points throws on a source it cannot
178+
# read: `ts.createSourceFile` returns a tree built by error recovery with
179+
# the errors parked on `parseDiagnostics`; `ts.createProgram` parks them
180+
# behind a second call, `getSyntacticDiagnostics()`; `ts.transpileModule`
181+
# reports nothing at all without `reportDiagnostics: true` and still
182+
# hands back an `outputText`. A gate then walks the wreckage, finds none
183+
# of the shapes it is looking for, and scores the file CLEAN — so the
184+
# SYMPTOM OF A MISSING REFUSAL IS A GREEN LINE, and an unguarded gate is
185+
# indistinguishable from a guarded one by reading CI. That is not a
186+
# theory: one gate here forced `ScriptKind.TSX` on 2504 test files, read
187+
# 32 of them as wreckage, and printed `OK` while six pinned engine
188+
# doubles went uncounted.
189+
# The #10573 sweep converted 32 call sites across 15 gates; it could not
190+
# stop the sixteenth being typed, and within the hour a new gate landed
191+
# with two raw calls in it — caught by this step, which is the whole
192+
# argument for having it. Same shape as `check:entry-guard` above.
193+
# Also prints the parses OUTSIDE `scripts/**` (#10575) that it does not
194+
# govern, so its green line is read as a claim about `scripts/` and not
195+
# about the repository.
196+
# Scans ~121 scripts/ files plus a read-only census of the rest, no
197+
# spawns; ~0.6s.
198+
- name: scripts/ TypeScript parses go through one module
199+
run: pnpm check:parse-guard
200+
175201
# Stack-collection enumerations vs the schema (#6242). `stack.zod.ts`
176202
# decides which collections a stack may declare; eight other enumerations
177203
# of that same set are hand-maintained (the map-format list, the
@@ -441,14 +467,25 @@ jobs:
441467
#
442468
# Nothing in CI *uses* the lock (GitHub runners are one job per machine),
443469
# so this step exists to keep the mechanism honest rather than to serialise
444-
# anything: 29 cases over the budget clamp, ticket liveness (dead pid,
470+
# anything: 48 cases over the budget clamp, ticket liveness (dead pid,
445471
# REUSED pid, over-age), fd hygiene, exit-code passthrough, and real
446472
# multi-process contention — queue timeout, legacy free-hand coexistence in
447473
# both directions, and three staggered waiters acquiring in arrival order.
474+
#
475+
# Since #10289 it also guards the BASH 3.2 FLOOR and the bounded-refusal
476+
# rule, which is why the count moved: `/usr/bin/env bash` is 3.2.57 on
477+
# macOS, and a bash 4+/5+ construct there does not fail loudly — it makes
478+
# the acquisition deadline unreachable and turns the wait into an unbounded
479+
# spin that prints no VERDICT at all. So the suite now scans the
480+
# acquisition path for 4+/5+ constructs, runs a real acquisition with
481+
# EPOCHSECONDS/EPOCHREALTIME unset and mapfile disabled, and asserts that
482+
# an unusable host (no `flock`) and a stopped clock each REFUSE with a
483+
# verdict and a non-zero exit rather than waiting forever.
484+
#
448485
# It runs entirely on a private lock under a temp dir; it never touches
449486
# /tmp/os-heavy-verify.lock, so a runner and an agent container behave the
450487
# same. Unconditional and un-`if:`-ed, like the self-tests above — an
451-
# exemption is what a self-test must not have. No network, ~15 s.
488+
# exemption is what a self-test must not have. No network, ~40 s.
452489
- name: Verify-lock entry-point self-test
453490
run: bash scripts/pm/os-verify-lock.sh --self-test
454491

@@ -1329,10 +1366,42 @@ jobs:
13291366
- name: Shard partitioner self-test
13301367
run: node scripts/partition-test-shards.mjs --self-test
13311368

1369+
# Hand-written `.d.mts` mirrors (#10549). `scripts/js-comment-mask.mjs`
1370+
# and `scripts/check-regen-pending.mjs` are untyped `.mjs` that each ship
1371+
# a hand-written declaration beside them, and both files say "keep this in
1372+
# step with the module by hand". Nothing checked that they were. A
1373+
# `.d.mts` has no runtime existence, so nothing executes it and nothing
1374+
# notices: TypeScript consumers see ONLY the declaration, so a drift makes
1375+
# every consumer type-check GREEN against a signature the module does not
1376+
# implement, first symptom a runtime failure downstream. That this file
1377+
# silently decides typecheck outcomes is measured rather than theoretical
1378+
# — PR #10513 went red with `TS2578` on two lanes purely because the
1379+
# mirror existed on the merged tree and not on the branch's, and that
1380+
# episode cost PR #10450 two merge-queue evictions. This gate asserts
1381+
# name, kind and required arity per declared export, and it DISCOVERS its
1382+
# corpus (every `scripts/**/*.d.mts`), so a third mirror added tomorrow is
1383+
# covered by existing rather than by anyone remembering to enrol it.
1384+
# Invoked as `node` rather than through a `pnpm check:*` alias for the
1385+
# same reason as the two steps above: that alias belongs in root
1386+
# package.json, declared territory of the @changesets/cli v3 lane (#9465)
1387+
# while it runs. dispatch-gates.mjs derives gate families from either
1388+
# spelling. Imports two small modules; milliseconds.
1389+
- name: Hand-written declaration mirrors
1390+
run: |
1391+
node scripts/check-declaration-mirrors.mjs --self-test
1392+
node scripts/check-declaration-mirrors.mjs
1393+
13321394
# The inventory of `packages/**` tests coupled to `examples/**` (#8754).
1333-
# Sibling of the gate above, on the axis it cannot see: that one detects
1334-
# tests whose FILESYSTEM READS escape their package, this one detects
1335-
# tests that IMPORT an example app live. `packages/cli` dynamically
1395+
# Sibling of the cross-package gate above, on the axis that gate does not
1396+
# own. ⚠️ The line that used to stand here — "that one detects tests whose
1397+
# FILESYSTEM READS escape their package, this one detects tests that
1398+
# IMPORT an example app live" — stopped being true in #10452, which taught
1399+
# the cross-package gate to read escaping import SPECIFIERS as well as
1400+
# path-shaped reads. Both now see the same couplings and do different
1401+
# things with them: the cross-package gate turns one into a declared input
1402+
# radius plus the turbo glob that hashes it, while this gate keeps the
1403+
# `examples/**` INVENTORY and grades what CI can see of each entry.
1404+
# `packages/cli` dynamically
13361405
# imports `examples/app-showcase/src/ui/views/contact.view` and asserts
13371406
# `toEqual` over a hardcoded `_sections` key list; `packages/lint`
13381407
# statically imports the same app's `Contact`/`ContactViews`. Neither
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* FIXTURE — not part of this app's suite.
5+
*
6+
* `vitest.config.ts` excludes `test/fixtures/**`, so a normal
7+
* `pnpm --filter @objectstack/example-showcase test` never collects this file.
8+
* It is run only by `test/vitest-console-teardown-race.test.ts`, which spawns
9+
* vitest against THIS directory as its root (at which point the exclude no
10+
* longer matches, because the path is relative to the root being used).
11+
*
12+
* WHAT IT REPRODUCES. The file passes its one assertion and then leaves a
13+
* `console.log` rescheduling itself past the end of the file — the shape #9371
14+
* had (a messaging dispatcher that outlived its test file) and the shape any
15+
* leaked timer, poll or fire-and-forget write has. In a worker whose console is
16+
* intercepted, each of those logs is an `onUserConsoleLog` RPC whose promise
17+
* vitest discards, so one landing inside the teardown window is rejected with
18+
* `EnvironmentTeardownError` and nobody holds it — an unhandled rejection, and
19+
* vitest fails a run on an unhandled error even with zero failed assertions.
20+
*
21+
* ⛔ Do not "fix" the leak here. The leak IS the instrument.
22+
*/
23+
24+
import { it, expect } from 'vitest';
25+
26+
it('passes, and leaves a console.log rescheduling past the end of the file', () => {
27+
const tick = (): void => {
28+
console.log('late log from a callback that outlived the test file');
29+
setImmediate(tick).unref?.();
30+
};
31+
setImmediate(tick).unref?.();
32+
33+
expect(1).toBe(1);
34+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The ABLATION leg of `test/vitest-console-teardown-race.test.ts`.
5+
*
6+
* `disableConsoleIntercept: false` is vitest's own default, spelled out here
7+
* rather than left implicit: this config exists to state that the ONE variable
8+
* between the two legs is the guard, and to keep the leg honest if the default
9+
* ever changes upstream. It deliberately does not extend the app's real config
10+
* — the fixture imports nothing from the workspace, so the app's aliases and
11+
* excludes cannot affect the measurement, and re-exporting a config that reads
12+
* `__dirname` from a different directory would silently repoint them.
13+
*/
14+
15+
import { defineConfig } from 'vitest/config';
16+
17+
export default defineConfig({
18+
test: {
19+
disableConsoleIntercept: false,
20+
},
21+
});

0 commit comments

Comments
 (0)