Skip to content

Commit 1108cdc

Browse files
committed
Merge origin/main into claude/issue-12005-history-meta-item-declared
Re-anchor the eight rest-server.ts isSystem citations on the merged tree: both sides had re-anchored for their own shifts, so the merged file needed line numbers re-derived by the census (check-system-context-census: OK). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N
2 parents aad3d73 + ff37576 commit 1108cdc

190 files changed

Lines changed: 11199 additions & 695 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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
fix(plugin-auth): report the refused identity writes ten `catch {}` sites swallowed (#12981)
6+
7+
Batch 5 of the ruled `catch { return null; }` worklist, scoped to `plugin-auth`.
8+
A re-run of the census instrument
9+
(`scripts/measure-durability-swallow-family.mjs`) moves tier-1 DARK from
10+
**21 sites in 11 files to 11 in 10**, with `LOUD` rising **32 → 34** and the
11+
adjacent `QUIET` bucket **85 → 93** — the ten, moved, nothing else touched.
12+
13+
Every site swallowed a refused write into a bare `catch` whose comment said
14+
"best-effort". That was true about **control flow** — a bookkeeping write must
15+
never turn a valid sign-in into a 500 — and it was being read as permission to
16+
say nothing, which is a different decision. The request answered 200, the
17+
session was issued, and nothing downstream looked wrong; the write simply did
18+
not happen. In `plugin-auth` that silence was hiding **security controls that
19+
had quietly stopped enforcing**.
20+
21+
**What an operator now sees that they did not before** — one line per seam,
22+
each naming the object that did not land, the control that is consequently not
23+
enforced, and the remedy:
24+
25+
- **`recordSignInOutcome`** — a refused `failed_login_count` / `locked_until`
26+
write means consecutive failures never accumulate and the account is never
27+
locked: `lockoutThreshold` (ADR-0069 D2) is configured and not enforced, so a
28+
brute-force run against that account meets no limit.
29+
- **`recordPasswordHistory`** — a refused ring write means the password just
30+
replaced was never recorded, so `passwordHistoryCount` (ADR-0069 D1) will not
31+
refuse it next time. "You cannot reuse your last N passwords" is advertised
32+
and, for that identity, not enforced.
33+
- **`enforceSessionControls`** — a refused revocation leaves a session that IS
34+
past its idle / absolute limit fully live (ADR-0069 D4). A refused activity
35+
heartbeat is reported separately, because it fails the other way: the idle
36+
clock keeps measuring from an older instant, so an active user is signed out
37+
**earlier** than the configured window.
38+
- **`enforceConcurrentCap`** — a refused revocation leaves the account holding
39+
more simultaneous sessions than `maxConcurrentSessions` allows.
40+
- **`stampIdentitySource`** (and its SCIM twin in `auth-plugin.ts`) — a refused
41+
provenance stamp leaves a federated identity still reading `env_native`, so it
42+
is offered the local-password actions that are supposed to hide for a managed
43+
identity (cloud ADR-0024 D4) — the path by which a managed user self-mints a
44+
password that bypasses enforced SSO. The SCIM hook is the sharper of the two:
45+
it is the **only** stamp on the adapter-level path and nothing retries it.
46+
- **`unlockUser`** — the password stage is cleared and the method still answers
47+
`true`, so the admin is told the unlock worked while a user locked at the
48+
second factor stays locked with no escape hatch at all. That `true` is
49+
exactly why the silence had to go.
50+
- **`stampPasswordChangedAt`** — a refused write leaves any admin-issued
51+
force-change flag SET (so the user is told to change a password they just
52+
changed) and leaves the password-age policy reading a timestamp for a password
53+
that no longer exists.
54+
- **`stampLastLogin`** — this write is what plugin-audit turns into the
55+
change-trail row, so a refusal leaves the sign-in with **no** trace in the
56+
compliance ledger at all.
57+
58+
Three inline `.catch(() => undefined)` swallows inside the two session controls
59+
are repaired in the same change. They are not optional to it: they sit between
60+
the refused write and the enclosing `catch`, so repairing only the outer handler
61+
would have produced a reporter that could never fire — a green-looking fix over
62+
an unchanged silence.
63+
64+
Level: the eight `AuthManager` seams report at `warn`, not `error`.
65+
`AuthManagerOptions.logger` declares `{ info?; warn }` with no `error` and is
66+
re-exported from the package `index.ts`, so adding one is a published-shape
67+
change; #12981 routes that question to #13398 and scopes this batch to the
68+
**silence**, exactly as batches 1 and 2 did for `plugin-security`. The two
69+
`AuthPlugin` seams log through the kernel `Logger`, whose `error` is required,
70+
and use it.
71+
72+
No entry was added to `scripts/durability-degradation.baseline.json`, and the
73+
gate vocabulary is untouched in either direction.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
"@objectstack/core": minor
3+
"@objectstack/types": minor
4+
"@objectstack/metadata": patch
5+
"@objectstack/rest": patch
6+
"@objectstack/service-datasource": patch
7+
"@objectstack/service-settings": patch
8+
"@objectstack/service-storage": patch
9+
"@objectstack/plugin-sharing": patch
10+
"@objectstack/cloud-connection": patch
11+
---
12+
13+
fix(core,rest,services)!: a permission-store read failure now fails LOUD instead of resolving as an authenticated caller holding zero capabilities (#13279)
14+
15+
**BREAKING** runtime behaviour change on the shared authorization resolver,
16+
shipped as `minor` under the repo's launch-window convention.
17+
18+
`resolveAuthzContext`'s per-read helper `tryFind` answered a THROWN read exactly
19+
the way it answered an EMPTY one: `[]`. So an outage of the permission store
20+
resolved as a well-formed context for an authenticated principal holding no
21+
capabilities, and the package-management door answered
22+
`403 FORBIDDEN` — "Reading packages requires the `studio.access` or
23+
`setup.access` capability." That answer was measured byte-identical
24+
(`JSON.stringify` equal, against a control that separates two answers which do
25+
differ) to what a caller who genuinely holds nothing receives. An administrator
26+
was told they lack a capability, during an outage of the store that holds the
27+
capability.
28+
29+
Maintainer ruling 2026-08-30, verbatim 「第一批其余同意」: `tryFind` 区分「无行」
30+
与「读失败」,读失败 fail-loud —— 权限库不可达时不再解析为「已认证零能力」,而是
31+
响亮拒绝(与真实能力拒绝的 403 可区分)。
32+
33+
Second maintainer ruling the same day (第 5 场总监席决裁批 #9, verbatim 「同意」),
34+
after implementing the first one showed that "the read failed" is two facts:
35+
**选项 A** —— 把 `isMissingTableError``@objectstack/metadata` 迁至
36+
`@objectstack/types`(core 已依赖),metadata 保留 re-export 兼容;`tryFind` 仅对
37+
**未被判定为「表未 provision」**的读失败抛 `AuthzStoreUnavailableError`
38+
39+
**What changed.** A permission-store read that is issued and throws now raises
40+
`AuthzStoreUnavailableError`, which carries the EXISTING ADR-0112 wire code
41+
`SERVICE_UNAVAILABLE` and status `503`. No code is added to the closed wire
42+
vocabulary and no response envelope gains or loses a key — only which declared
43+
code an outage selects. Doors that map thrown errors through
44+
`resolveThrownHttpError` answer 503 with no per-door change.
45+
46+
**What did NOT change**, and is pinned:
47+
48+
- A reachable, genuinely EMPTY store (reads return no rows) still resolves to
49+
zero capabilities.
50+
- A genuine capability denial still answers `403 FORBIDDEN` with its message.
51+
- An ABSENT engine (`ql` unwired, so no read is ever issued) still resolves to
52+
an empty-but-valid envelope.
53+
- Anonymous requests never reach the store, so an outage cannot make them loud.
54+
- A REAL engine whose `sys_*` tables were never provisioned resolves to zero
55+
capabilities, quietly — pinned to be byte-identical to the empty-store
56+
envelope, in every dialect spelling and in the production wrapper shape where
57+
the driver's phrase is on `cause` rather than the outer message.
58+
59+
**The boundary between the two kinds of read failure.** An earlier revision of
60+
this changeset claimed "embedders without a data plane are unaffected". That
61+
claim was too broad; it is retracted here, and the gap it named is now closed
62+
rather than merely disclosed. A read also throws when the table was never
63+
PROVISIONED — a real engine, wired and reachable, whose `sys_*` tables were
64+
never created — and that is a supported deployment shape, not an outage. There
65+
"zero capabilities" is the TRUE answer rather than a fabrication: nothing is
66+
provisioned, so nothing was withheld. Only an UNREACHABLE store — the ruling's
67+
own word 不可达 — leaves the capability set unknown, and only an unknown answer
68+
may not be reported as a denial.
69+
70+
Treating the two alike was measured, not theorised: it turned four CI suites
71+
red, all from `no such table` on `sys_user` / `sys_member` /
72+
`sys_user_position` / `sys_user_permission_set`. Ordinary CRUD in
73+
`@objectstack/client` answered `503`; batch validation errors that owe `400`
74+
answered `503`, because authorization refused before validation ran; runtime
75+
notifications answered `401` where authenticated callers must be served `200`;
76+
and two `.integration.test.ts` noise guards reported that the driver and engine
77+
diagnostics for `sys_position` stopped being emitted — the eager throw aborted
78+
the resolution before that later read was ever issued, so a change made to stop
79+
a failed read being silent had made two other channels silent.
80+
81+
`tryFind` therefore raises `AuthzStoreUnavailableError` only for a read failure
82+
that is NOT positively identified as an unprovisioned table.
83+
84+
**`isMissingTableError` moved to `@objectstack/types`.** The classifier that
85+
draws that boundary already existed and was already right — driver-code based
86+
rather than prose-sniffing, documented so that "cannot say" never means "be
87+
loud". It lived in `@objectstack/metadata`, which DEPENDS ON `@objectstack/core`,
88+
so the resolver could not import it. Rather than keep a second copy of a
89+
security-relevant predicate, the ruling relocated the one classifier to
90+
`@objectstack/types` — the package core already depends on, and the repo's own
91+
stated Home rule for a cross-package error predicate ("every consumer of the
92+
question already depends on it, so adopting the predicate never adds an edge",
93+
`packages/types/src/unique-violation.ts`). `@objectstack/metadata/errors` still
94+
exports `isMissingTableError`, re-exported from the new home, so no consumer of
95+
that published subpath changes.
96+
97+
Its sibling `isSchemaAlreadyExistsError` moved with it — the two are not two
98+
modules but two signatures over one matcher, and separating them would have
99+
meant re-rolling the matcher, which is the duplication the module exists to
100+
prevent. Both are now exported from `@objectstack/types`; the metadata subpath
101+
deliberately still publishes only `isMissingTableError`, which is the only one
102+
anything imports through it.
103+
104+
⚠️ **Signed-off risk, recorded because it is load-bearing.** Gating loudness on
105+
a driver-error predicate was approved with its false-positive direction stated:
106+
mis-reading a genuine outage as "table not provisioned" silently restores the
107+
quiet 403 this change removes, with no thrown error and no other failing test.
108+
That direction is accepted, not overlooked — the predicate keys on driver codes,
109+
SQLSTATEs and errnos first, excludes the known superstring traps up front, and
110+
returns `false` for anything it does not positively recognise, so an
111+
unrecognised outage stays loud by default. The risk is written beside the
112+
predicate in `resolve-authz-context.ts` and both directions are pinned by name
113+
in `authz-store-unavailable.test.ts`. ⛔ Do not widen `isMissingTableError` to
114+
make a first boot quieter: every widening moves outages into the quiet branch.
115+
116+
**All-transport, not just REST.** Every transport authorizing through
117+
`resolveAuthzContext` inherits this. Six of the eight production transports
118+
wrapped the call in a fail-closed `catch` that would have re-silenced the
119+
outage — measured, not assumed: with the resolver loud but the nets untouched,
120+
the package door answered `401`, i.e. the outage merely changed disguises. Those
121+
`catch` blocks now re-raise via `isAuthzStoreUnavailableError` and keep their
122+
previous behaviour for every other fault. The transport set is rebuilt from
123+
source and audited for set equality on every test run, so a transport added
124+
later cannot inherit the old silence unnoticed.
125+
126+
Callers that treat any throw from `resolveAuthzContext` as "anonymous" should
127+
re-raise `isAuthzStoreUnavailableError(err)` instead: degrading it restores the
128+
disguise this removes.
129+
130+
<!-- adr-0087: not-required (runtime-interface-only packages/core/src/security/resolve-authz-context.ts#ResolvedAuthzContext, packages/core/src/security/authz-store-unavailable.ts#AuthzStoreUnavailableError) The breaking surface is runtime TypeScript in `@objectstack/core`'s security module and nothing else: `resolveAuthzContext` stops always-resolving and raises `AuthzStoreUnavailableError` when a permission-store read is issued and throws. NO metadata surface is touched in either direction. No Zod schema changes, no `packages/spec` declaration is added or removed, no authorable key moves, no stored row shape changes, and no object definition is edited — a customer's metadata app is byte-for-byte unaffected, so `objectstack migrate meta` has nothing to visit and there is no tombstone to mint. The wire vocabulary is likewise untouched: `SERVICE_UNAVAILABLE` is an EXISTING `StandardErrorCode` member that `HttpStatusErrorCodeMap` already maps to 503, so this change only selects a different DECLARED code for an outage rather than adding one. Both named symbols resolve at HEAD as exported declarations whose files are not `*.zod.ts`, are not under `packages/spec/src/contracts/`, are not object definitions and are not `z.input` projections; neither is referenced in code by any metadata surface (the `packages/spec` hits for `resolveAuthzContext` are comment prose describing the envelope, which this gate masks). The channel that reaches an affected consumer is therefore code review and this changeset, never the upgrade guide: a ledger entry could not express "your fail-closed catch should re-raise this error", because there is no metadata for a migration to rewrite. -->
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
"@objectstack/cloud-connection": patch
3+
"@objectstack/lint": patch
4+
"@objectstack/mcp": patch
5+
"@objectstack/metadata-core": patch
6+
"@objectstack/metadata-fs": patch
7+
"@objectstack/metadata-protocol": patch
8+
"@objectstack/metadata": patch
9+
"@objectstack/observability": patch
10+
"@objectstack/plugin-webhooks": patch
11+
"@objectstack/rest": patch
12+
"@objectstack/runtime": patch
13+
"@objectstack/service-analytics": patch
14+
"@objectstack/service-automation": patch
15+
"@objectstack/service-cache": patch
16+
"@objectstack/service-cluster-redis": patch
17+
"@objectstack/service-cluster": patch
18+
"@objectstack/service-datasource": patch
19+
"@objectstack/service-i18n": patch
20+
"@objectstack/service-job": patch
21+
"@objectstack/service-knowledge": patch
22+
"@objectstack/service-messaging": patch
23+
"@objectstack/service-package": patch
24+
"@objectstack/service-queue": patch
25+
"@objectstack/service-realtime": patch
26+
"@objectstack/service-settings": patch
27+
"@objectstack/service-storage": patch
28+
"@objectstack/verify": patch
29+
---
30+
31+
fix(build): give each `exports` condition its own `types` target in the 28 dual-build packages (#13112)
32+
33+
**Published-surface change, zero runtime change.** No emitted byte moves; what
34+
moves is which declaration file a resolver READS. Maintainer ruling 2026-08-29
35+
(decision batch #3, verbatim 「同意」) chose declaring the files over deleting
36+
them.
37+
38+
## What was wrong
39+
40+
These 28 packages are `"type": "module"` and dual-built, and each spelled one
41+
`types` condition as a **sibling** of `import`/`require`:
42+
43+
```json
44+
"exports": { ".": {
45+
"types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs"
46+
} }
47+
```
48+
49+
A sibling `types` answers for **both** conditions, so a CommonJS consumer was
50+
handed `dist/index.d.ts` — an ES-module declaration, because the package is
51+
`"type": "module"` — for an entry point it reaches with `require`. Measured with
52+
`tsc --traceResolution` on a `"type": "commonjs"` fixture at `moduleResolution:
53+
node16`:
54+
55+
```
56+
error TS1479: The current file is a CommonJS module whose imports will produce
57+
'require' calls; however, the referenced file is an ECMAScript module and cannot
58+
be imported with 'require'.
59+
```
60+
61+
The JavaScript at `dist/index.cjs` loads perfectly (`check:dual-build-cjs-loads`
62+
has asserted that for months). It is the **types** that told the consumer the
63+
supported `require` entry point could not be required. The `dist/index.d.cts`
64+
twin tsup emits beside it — 36 files, 5,517,701 B on this build — was named by
65+
no condition at all and shipped in every tarball unreachable.
66+
67+
## What changed
68+
69+
Each condition now names its own declaration, the shape TypeScript documents:
70+
71+
```json
72+
"exports": { ".": {
73+
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
74+
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
75+
} }
76+
```
77+
78+
33 entry points across 27 packages, subpaths included. The root `types` field is
79+
untouched, so `node10` resolvers are unaffected; the `import` condition resolves
80+
exactly what it resolved before, measured as an unchanged control in the same
81+
run.
82+
83+
## `@objectstack/core` is deliberately NOT changed
84+
85+
Splitting a declaration in two makes TypeScript compare it nominally, and
86+
`ObjectKernel` carries a `private plugins` member that reaches every plugin
87+
through `PluginContext.getKernel()`. With core split, whole-repo `pnpm build`
88+
fails in `@objectstack/verify` with 5 × TS2345 ("Types have separate
89+
declarations of a private property 'plugins'"); with core held back and the
90+
other 27 split, 71/71 tasks pass. So core keeps the sibling-`types` shape and
91+
its two `.d.cts` files (220,854 B) stay unreachable, declared as such in
92+
`check:dual-build-cjs-loads`. Splitting it needs a decision about core's public
93+
types, not about an exports map.
94+
95+
## For consumers
96+
97+
- **ESM consumers: nothing changes.** Same declaration file, byte for byte.
98+
- **CJS consumers under `node16`/`nodenext`: TS1479 goes away** and the
99+
declarations they get are the ones built for CommonJS.
100+
- **`node10` / `moduleResolution: node` consumers: nothing changes** — they never
101+
read `exports`.
102+
- Nothing is removed: every path that resolved before still resolves.
103+
104+
Packages that are CJS-first (`require``./dist/index.js`, no `"type": "module"`)
105+
were already correct and are untouched — their `dist/index.d.ts` really is the
106+
CommonJS declaration. Their ESM mirror (an unreachable `.d.mts` under the
107+
`import` condition) is a separate, larger population and is filed separately per
108+
the ruling, not fixed here.
109+
110+
`check:dual-build-cjs-loads` grew a fourth invariant (TYPED) that reds on the old
111+
shape, so the drift cannot return silently.

0 commit comments

Comments
 (0)