You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 9d60b42
Browse filesBrowse the repository at this point in the historyBrowse files
fix(service-settings)!: a host that binds its own engine and states no organization source reads and writes as the organization D9 derives (#25350)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuSQfvzuV2ZjD9aZ44eAhnVq)_Fixes#25249
Clause-②: no (narrowing)
Metadata-protocol: none
Ruling-ref: 6355249103 (letter A). Why `Clause-②: no (narrowing)`:
nothing is added to any accept set. `SettingsServiceOptions` and the
`bindEngine` extras are unchanged. What narrows is which organization a
host that binds its own engine, and states no organization source, reads
and writes as.
## What changed
`SettingsService.organizationReachOf`
(`packages/services/service-settings/src/settings-service.ts`) now takes
ADR-0131 D9's answer from `organizationDecision()`:
1. the stated `organization` source, when the host bound one (the plugin
always does);
2. no engine at all (the in-memory store): `no-organization-object`, as
before;
3. otherwise `resolveGuestOrganization(this.engine, posture)`, the same
call the plugin's source makes, through the engine the service holds.
There is no second predicate, no registry-sniffing arm, and no `catch`
around the probe. An engine that exposes no `registry` (the plugin's own
`wrapEngineAsSettingsEngine`, any hand-written `SettingsEngine`) cannot
say the organization object is absent, so D9 reads it as registered and
asks `sys_organization`. A lean self-binding host declares the
composition at its bind site: `bindEngine(engine, audit, { organization:
async () => ({ kind: 'no-organization-object' }) })`, or hands over an
engine whose `registry.getObject` answers.
The retired docblock sentence ("Unset … reads as a composition that
registers no organization object") is rewritten on the source type, its
field and the `bindEngine` extra. The `index.ts` host paragraph gains
the declaration line and names the symptom of omitting it. No `docs/adr`
edit.
Also: a changeset (`@objectstack/service-settings` patch, BREAKING,
FROM/TO, the one-line fix).
## Branch
The harness assigned the branch `claude/project-thread-c4vx2u`, not the
repo-convention `claude/issue-25249-…`. This note is for the Duplicate
Fix Guard. The card's `Claim:` (6354964172) names this branch.
## Tests
Premises P1–P4 of the ruling were re-read on the branch before the
migration: `registryHoldsObject` answers `true` with no
`registry.getObject` (core `resolve-authz-context.ts:1235`);
`wrapEngineAsSettingsEngine` returns `find` / `insert` / `update` only
and forwards `find` verbatim; the plugin's only `bindEngine` call states
a source; the plugin is the only non-test constructor in the repo. All
four hold.
**Migration (45 tests in 7 files, red on the route alone).** Each suite
now states its composition truthfully, through a shared testkit
(`src/__tests__/settings-organization.testkit.ts`):
- a suite over a double states `LEAN_ORGANIZATION_SOURCE` at its bind:
`settings-getmany`, `settings-user-rung-owner.pin`,
`settings-system-context.pin` (its constructor-engine case moves to
`bindEngine` with the same crypto provider and secret store);
- a suite running the real `ObjectQL` behind the production adapter
forwards that engine's `registry` (`withEngineRegistry`), so D9 answers
`no-organization-object` itself: `settings-secret-rotation`,
`sys-secret-orphan-report`, `settings-global-rung`,
`settings-user-reference.pin`.
No default in the service, no stub `getObject`, no new input. The four
construction sites outside the package pass `{ env }` and no engine;
they are untouched.
**Pins** (`settings-organization-reach.pin.test.ts`, new section):
- (a) a registry-less adapter over a real engine holding one
`sys_organization` row, no source: the tenant and user rungs read and
write as that organization, and the rows with no organization do not
answer;
- (b) no organization object, no source: on the real engine,
`AuthzStoreUnavailableError` naming `sys_organization` (pinned as
measured; `isMissingTableError` not widened); on a double whose probe
answers nothing, no tenant rung and the tenant write refused
(`SETTINGS_VALIDATION`);
- (c) a stated lean source: rows with no organization read and written,
unchanged;
- (d) no engine: the in-memory store unchanged;
- (e) the existing plugin pins keep their assertions.
Ablation (`scripts/ablation-replace.mjs` on the committed route): the
engine branch of `organizationDecision` replaced by the old lean answer.
(a) and both (b) pins went red; (c), (d) and the plugin pins (e) stayed
green, the expected direction. Restored: blob equals HEAD, `git diff
HEAD` empty.
Runs on `e12143e6` (after merging `origin/main`):
- `vitest run` in `packages/services/service-settings`: 58 files / 987
tests passed (982 before the new pins).
- `pnpm --filter @objectstack/service-settings typecheck`: exit 0.
- Derived gates, the full dogfood battery (`!` title) and the
contract-review-tier review: reported on the card in the os-dev-report
and on this PR.
## Out of scope
Hosts outside this repo that bind `SettingsService` themselves (cloud
control-plane mocks, embedded runtimes) are downstream of the published
package. The changeset's FROM / TO and its one-line fix are the
hand-off; no cloud card is filed from here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_014FFTcf6msoLbXiD6BR7wHW)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_014FFTcf6msoLbXiD6BR7wHW
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(service-settings)!: a host that binds its own engine and states no organization source reads and writes as the organization ADR-0131 D9 derives through that engine, not as no organization
6
+
7
+
Clause-②: no (narrowing)
8
+
Metadata-protocol: none
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) Which organization a self-binding host's settings reads and writes take changes. No key of any metadata schema is removed, renamed or re-shaped, so `objectstack migrate meta` has nothing to rewrite and no tombstone is needed. No export, type, option, error code or route is added, removed or renamed: `SettingsServiceOptions` and the `bindEngine` extras are unchanged, and `SettingsServicePlugin` already states its source. The remedy for a host this narrows is one line at its bind site, stated below. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this read (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
11
+
12
+
**BREAKING** read narrowing (ADR-0131 D8/D9, follow-up to the organization-less rung fix). It changes which organization a host's settings reads take, not what a metadata author can write, so it ships as `patch` with this banner.
13
+
14
+
- FROM: a host that bound its own engine (`new SettingsService({ engine })` or `bindEngine(engine)`) and stated no `organization` source read and wrote, for a caller that names no organization, as a composition with no organization object: its rows with no organization answered, and its writes stored none.
15
+
- TO: such a host's service asks D9 itself, through the engine it holds (`resolveGuestOrganization`, the call the plugin's source makes). Under `single` with one organization it reads and writes as that organization; at none or several it has no tenant or user rung. An engine that exposes no `registry` cannot say the organization object is absent, so D9 asks `sys_organization`. Over a store with no such object, a tenant-rung read or write by a caller naming none is then refused as organization-less (reads answer from the global rung or the default, tenant writes are refused with `SETTINGS_VALIDATION`), or raises `AuthzStoreUnavailableError` naming `sys_organization` where the engine does not know that object. No engine at all (the in-memory store), a stated source, an engine whose `registry.getObject` answers, and `SettingsServicePlugin` (`os serve`, `os dev`) are unchanged.
16
+
17
+
**Who is affected.** Only a host that binds its own engine and states no source: a control-plane mock or an embedded runtime. On a composition with an organization object it now reads and writes the derived organization's rows, and its rows with no organization stop answering. On one without, it meets the refusal above. A deployment booted through `SettingsServicePlugin` is not affected.
18
+
19
+
**The fix.** State the composition at the bind site: `service.bindEngine(engine, audit, { organization: async () => ({ kind: 'no-organization-object' }) })` (a host that hands its engine to the constructor binds it this way instead), or hand over an engine whose `registry.getObject` can answer.
0 commit comments