Repository navigation
Commit d196748
fix(core)!: the localization sys_setting fallback reads the caller organization only (ADR-0131 D8) (#25301)
Fixes #25248
Part of #15212. Filed from the contract review of #25226 (record
6343868210, ③ 3a). Claim: 6350248910. Owed under ADR-0131 D8.
## Measurement (step 1, on `main` at `1eb1afae07`)
`resolveLocalizationContextUncached` reads `sys_setting` directly when
the settings service does not answer. On `main` that read is `scope:
'tenant'` with no organization filter, and it takes the first row per
key.
| how the fallback is reached | reached on a served composition? | leaks
on `main`? |
|:--|:--|:--|
| settings service absent | only `sys_setting`'s own registrar is
`@objectstack/service-settings` (`manifest.ts`), so without it the read
fails ("object not found"), memoized as a backend fault, and `UTC` /
`en-US` answer | no rows to leak, unless a host registers `sys_setting`
itself |
| `getMany` throws: `localization` namespace not registered yet (bind
window), or a partial manifest | yes | **yes** |
| `getMany` throws: the organization source's
`AuthzStoreUnavailableError` (#25226, a caller that names no
organization under `single`), or the posture source throws | yes |
**yes** |
| a healthy service | never: the manifest's `UTC` / `en-US` defaults
always answer, so the fallback is skipped | no |
| lean embedding (no `sys_organization`) | when the service throws, as
above | NULL rows are the legitimate population there |
| each posture | the fallback ignored the posture entirely | **yes**,
under `single`, `group` and `isolated` |
**Two organizations, each with a tenant `localization.timezone` row: can
a user of one read the other's?** Yes. A scratch probe (not committed)
used #25226's rig: a real `ObjectKernel` + `ObjectQL` +
`SettingsServicePlugin`, with only the driver a Map.
- `isolated`, `org_a` and `org_b`, the settings service throwing
(namespace not bound), caller `org_a`: `main` answers **`Asia/Tokyo`**
(`org_b`'s row). This branch answers `Europe/Paris` (`org_a`'s).
- `single`, two organizations, the organization read failing, caller
names none: `main` answers `org_b`'s `Asia/Tokyo`. This branch answers
`UTC`.
- Control, a healthy service: both answer `UTC`, and the fallback is not
reached.
The success cache can then hold a leaked answer for up to
`OS_LOCALIZATION_CACHE_TTL_MS`.
## What changes (step 2): scoped, not retired
The fallback now reads with the reach `SettingsService` reads with since
#25226 (`localizationFallbackReach`):
| caller | rows read |
|:--|:--|
| names organization X (any posture) | `organization_id = X` |
| names none, `single`, D9 derives X | `organization_id = X` |
| names none, `single`, zero or several organizations | none (built-ins)
|
| names none, walled (`group` / `isolated`) | none, and no query is
issued |
| names none, the organization read fails | none, never a guess |
| names none, no `sys_organization` registered (lean embedding, #25133
letter A) | `organization_id` NULL only |
- D9's answer comes from `resolveGuestOrganization`, the predicate in
this file that the settings plugin also wires.
- The posture is the **requested** one (`resolveTenancyPosture()`), read
the same way this file's platform-standing anchor reads it: no query,
and a deployment that asked for a wall stays walled while running
degraded. The fallback takes no posture input from the service it stands
in for.
- The organization goes into the `where` **and** is compared in memory,
following `preferredRow`'s rule.
- A failed organization derivation does not populate the failure memo.
It is not the direct read's own backend fault (#10221 / #11877
unchanged).
**Why not retire it.** Two things still use it. The #10221 failure memo
is keyed on this direct read and keeps the "no such table" log line from
repeating. And the bind-window answer is still the caller's own stored
value, which the #11580 re-resolve then replaces. Scoping keeps both,
with no other organization's row and no NULL row outside lean.
Clause-②: no (narrowing)
Metadata-protocol: none
A localization read stops admitting another organization's row, or a
NULL row outside lean embedding. The changeset ships `@objectstack/core`
as `patch` with the BREAKING banner, the FROM/TO and the remedy.
ADR-0087 disposition: `not-required (no-migration-prescription)`.
## Pins, and the ablation
`resolve-localization-fallback-organization.pin.test.ts` (new, 14
cases):
- own organization, with the settings service absent and throwing
`AuthzStoreUnavailableError`;
- no fallback to another organization, or to a NULL row;
- the in-memory compare, behind an engine that drops the filter;
- `single` with one, several or no organizations;
- `isolated` and `group`, for a caller naming none and a caller naming
its own organization;
- a failed organization read;
- lean.
Existing fixtures in `resolve-authz-context.test.ts`,
`resolve-localization-cache.test.ts` and plugin-hono-server's
`current-user-endpoints-localization.test.ts` gave their rows no
organization while their caller named `o1` / `org_lang`. Their rows now
carry the caller's organization. Nothing else in them changed.
Ablation, from the committed state, each restored with `git checkout
HEAD --` (`git status` clean afterwards):
| ablation | pins red |
|:--|:--|
| A1: `main`'s fallback (no organization at all) | **12 / 14** |
| A2: no in-memory compare | 1 / 14 (the filter-dropping engine) |
| A3: the wall ignored for a caller naming none | 2 / 14 (`isolated`,
`group`) |
| A4: the lean NULL arm removed | 1 / 14 (lean) |
## Proof (head `0139af1fe3`)
- **`@objectstack/core`:**
- `pnpm typecheck`: exit 0.
- vitest: 117 files / 2734 tests pass.
- **Downstream consumers:**
- plugin-hono-server: 448 / 448 after the fixture change, and `pnpm
typecheck` exit 0.
- mcp: 532 / 532.
- plugin-audit: 985 / 985.
- rest: 6607 pass, 344 skipped.
- **`node scripts/pm/dispatch-gates.mjs --commands`:** 70 derived. Each
exit code was captured before any pipe, and all 70 now exit 0.
- First pass: 66 exit 0.
- `check:objectql-double-limit` and `check:where-matcher` were red on
the new pin's engine double. They are fixed in `8c59890e8f` and
`0139af1fe3`, and both now exit 0.
- `check-plugin-teardown-shape --self-test` exited 3 (shallow clone).
After `git fetch --unshallow` it exits 0.
- `check:dual-build-cjs-loads` exited 3 (no full build). After `pnpm
build` it exits 0.
- **`check-governed-merges --branch`:** NOT governed (0 of 6 paths). 336
changed lines (+305 / −31). Not Tier H/S.
- **Upgrade harness** (`upgrade-18x-harness.dogfood.test.ts`, both
fixture legs and the fresh store): 16 / 16 pass.
- **R1, fresh boot:** a production `os serve` of the showcase on an
empty store under `single`:
- ready in 10 s, health 200;
- sign-up 200, sign-in 200;
- `GET /api/v1/auth/me/localization` 200 (`UTC` / `en-US`).
- **R4, a walled production `os serve`** (`NODE_ENV=production`,
`OS_PLATFORM_OWNER_EMAIL`, `OS_AUTH_MEMBERSHIP_POLICY=invite-only`, a
32-byte `OS_SECRET_KEY`), on an empty store:
- `isolated` and `group`: each ready in 9 s, health 200, sign-up 200,
sign-in 200, `me/localization` 200.
- `isolated`: `PUT /api/settings/localization` with `timezone:
Asia/Tokyo` answered 200, and the next `me/localization` read answered
`Asia/Tokyo`.
- ⚠ The showcase declares no `@objectstack/organizations`, and a walled
`os serve` refuses without it. For R4 only, the dependency was added
locally. `package.json` and the lockfile were restored afterwards. None
of it is in the diff.
## Acceptance notes
- `main` moved past `1eb1afae07` (to `f47f7f222f`). None of its commits
touch these files, so no merge was taken (§10).
- **Seat amendment · 2026-10-22T23:10Z** (epic PM
`session_01Y5583bs2RDd53MH6kM1ca3`). The body was written at head
`0139af1fe3`, and three facts above have aged:
- `Clause-②` is corrected from `yes (narrowing)` to `no (narrowing)`,
per the contract review 6353329341: the diff widens no surface. The
changeset line follows in the dev's next commit.
- The diff is now 7 files, +313 / −36. `417fe64988` adds
`packages/runtime/src/security/resolve-execution-context.test.ts`, a
fixture whose rows now carry the caller's organization.
- `main` was merged in at `83ff895f4b` (`origin/main` `09e07c259a`), so
the "no merge was taken" line above is superseded.
- The final head's proof is in the os-dev-report on #25248.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01DydJpxUXfktYEyRLvwbAEF
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 21a25dd commit d196748
7 files changed
Lines changed: 313 additions & 36 deletions
File tree
- .changeset
- packages
- core/src/security
- plugins/plugin-hono-server/src
- runtime/src/security
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
219 | 219 | | |
220 | 220 | | |
221 | 221 | | |
222 | | - | |
223 | | - | |
224 | | - | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
225 | 225 | | |
226 | 226 | | |
227 | 227 | | |
| |||
283 | 283 | | |
284 | 284 | | |
285 | 285 | | |
286 | | - | |
| 286 | + | |
287 | 287 | | |
288 | 288 | | |
289 | 289 | | |
| |||
505 | 505 | | |
506 | 506 | | |
507 | 507 | | |
508 | | - | |
| 508 | + | |
509 | 509 | | |
510 | 510 | | |
511 | 511 | | |
| |||
523 | 523 | | |
524 | 524 | | |
525 | 525 | | |
526 | | - | |
| 526 | + | |
527 | 527 | | |
528 | 528 | | |
529 | 529 | | |
530 | | - | |
| 530 | + | |
531 | 531 | | |
532 | 532 | | |
533 | 533 | | |
| |||
627 | 627 | | |
628 | 628 | | |
629 | 629 | | |
630 | | - | |
| 630 | + | |
631 | 631 | | |
632 | 632 | | |
633 | 633 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2386 | 2386 | | |
2387 | 2387 | | |
2388 | 2388 | | |
2389 | | - | |
| 2389 | + | |
| 2390 | + | |
| 2391 | + | |
2390 | 2392 | | |
2391 | 2393 | | |
2392 | 2394 | | |
| |||
2461 | 2463 | | |
2462 | 2464 | | |
2463 | 2465 | | |
| 2466 | + | |
| 2467 | + | |
| 2468 | + | |
| 2469 | + | |
| 2470 | + | |
| 2471 | + | |
| 2472 | + | |
| 2473 | + | |
| 2474 | + | |
| 2475 | + | |
| 2476 | + | |
| 2477 | + | |
| 2478 | + | |
| 2479 | + | |
| 2480 | + | |
| 2481 | + | |
| 2482 | + | |
| 2483 | + | |
| 2484 | + | |
| 2485 | + | |
| 2486 | + | |
| 2487 | + | |
| 2488 | + | |
| 2489 | + | |
| 2490 | + | |
| 2491 | + | |
| 2492 | + | |
| 2493 | + | |
| 2494 | + | |
| 2495 | + | |
| 2496 | + | |
| 2497 | + | |
| 2498 | + | |
| 2499 | + | |
| 2500 | + | |
| 2501 | + | |
| 2502 | + | |
| 2503 | + | |
| 2504 | + | |
| 2505 | + | |
| 2506 | + | |
| 2507 | + | |
| 2508 | + | |
| 2509 | + | |
| 2510 | + | |
| 2511 | + | |
| 2512 | + | |
| 2513 | + | |
| 2514 | + | |
| 2515 | + | |
2464 | 2516 | | |
2465 | 2517 | | |
2466 | 2518 | | |
| |||
2525 | 2577 | | |
2526 | 2578 | | |
2527 | 2579 | | |
| 2580 | + | |
| 2581 | + | |
| 2582 | + | |
| 2583 | + | |
2528 | 2584 | | |
2529 | 2585 | | |
2530 | 2586 | | |
| |||
2534 | 2590 | | |
2535 | 2591 | | |
2536 | 2592 | | |
2537 | | - | |
| 2593 | + | |
| 2594 | + | |
2538 | 2595 | | |
2539 | 2596 | | |
2540 | | - | |
| 2597 | + | |
| 2598 | + | |
| 2599 | + | |
| 2600 | + | |
| 2601 | + | |
| 2602 | + | |
2541 | 2603 | | |
2542 | 2604 | | |
2543 | 2605 | | |
2544 | 2606 | | |
2545 | | - | |
| 2607 | + | |
| 2608 | + | |
| 2609 | + | |
2546 | 2610 | | |
2547 | 2611 | | |
2548 | 2612 | | |
| |||
0 commit comments