Skip to content

fix(rest): the /references door forwards the caller's organization, so the "Used by" panel stops clearing a delete an org-scoped item depends on - #15689

Merged
hotlong merged 3 commits into
mainfrom
claude/issue-13753-references-door-org-forwarding
Sep 5, 2026
Merged

fix(rest): the /references door forwards the caller's organization, so the "Used by" panel stops clearing a delete an org-scoped item depends on#15689
hotlong merged 3 commits into
mainfrom
claude/issue-13753-references-door-org-forwarding

Conversation

@hotlong

@hotlong hotlong commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #13753

GET /api/v1/meta/:type/:name/references backs the admin "Used by" panel, whose empty case renders "Nothing in the metadata graph points at this item. Safe to delete." to an operator about to delete something. The door supplied no organization, so the reference sweep read the environment partition only: an org-scoped view pointing straight at the object being deleted was invisible and the panel issued a false clearance — the ADR-0110 D3 harm this route's own 501 refusal (#9326) was added to prevent, delivered by the door after the protocol had refused to deliver it.

The door now resolves the memoised exec ctx and passes the caller's organization RAW.

Why raw is correct and sufficient — verified on this branch, not inherited

The card's ⚠️ note prescribed the sibling doors' pre-gate, organizationIdForMetaRead(canonicalMetaUrlType(req.params.type), ctx?.tenantId). That is not what landed, and the reason is measured here rather than recalled:

  1. findReferencesToMeta spends the organization on the SOURCES. It resolves REFERENCE_SITES.byTarget.get(target), groups the sites by fromType, and reads each through this.getMetaItems({ type: matcher.fromType, ...organizationId })packages/metadata-protocol/src/protocol.ts:21343, the spread at :21490. req.params.type is the TARGET, so pre-gating on it would suppress the organization for exactly the object / flow / app deletes this card is about.
  2. The per-source-type gate is already the callee's. getMetaItems (protocol.ts:6876) computes const orgId = organizationIdForMetaRead(request.type, request.organizationId); on its OWN request type, after the canonical fold. That landed with the getMetaItems read gate whose stated purpose is multi-type sweeps scoped per type, and its in-code note names findReferencesToMeta as one of the three sweeps it repairs.
  3. organizationIdForMetaRead(type, org) (packages/metadata-core/src/meta-write-org-scope.ts) returns org only when declaresOrgOverride(type), else undefined.
  4. Re-checked as the card asked: request.organizationId has exactly one use inside findReferencesToMeta — that getMetaItems spread. Counted over the function body (lines 21343 to the next method declaration at 21550): two occurrences of the identifier, one being the request-type declaration itself. No second use, so passing it raw carries no other consequence.

⇒ An overridable source (view, dashboard, report, translation, email_template) honours the organization; every other source drops it and stays environment-wide, so no pre-#6190 phantom row is resurrected into a destructive-action clearance.

ADR-0131 D6/D7 retires the per-organization metadata partition in v18 (#15206, C5), so this is a repair inside a mechanism being removed — an existing value handed to an existing parameter, no new contract surface, and nothing is built on it.

Pins — red-first, and the red is the card's own defect

packages/rest/src/rest-server-meta-read-org-scope.test.ts, driving the real REST routes against a real ObjectStackProtocolImplementation over a stub engine, on one boot:

  1. The card. An object target with an org-scoped view referencing it (the view's object binding is a derived reference site). The write's partition is proved first (1 row under the org, 0 environment-wide), then the sweep must return the view.
  2. Narrowness control. On the SAME request, a non-overridable object SOURCE is read environment-wide: a planted pre-org 作用域的 flow overlay 只在「本进程内发布后」绑定触发器,重启后静默失绑——冷启动两条读路径都把 organization_id 非空的行滤掉了 #6190 phantom org row is NOT returned, while an environment-wide object source that references the target IS — so the exclusion is scope, not a broken walk.
  3. Nothing else moves. Cross-tenant control (org B is not served org A's view), org-less caller unchanged, environment-wide sources still served to an org caller, the response still exactly one references key with the unchanged row shape, and the [finding] The field metadata type can never match as a reference TARGET — items are keyed <object>.<field> while every reference site holds a bare field name #9327 unanswerable-target refusal keeps status: 501 / code: NOT_IMPLEMENTED.

Ablation (fix committed first, mutation and restoration both proved on disk by blob hash): restoring only rest-server.ts to the base commit fa125f3bf — worktree blob 3c66d49db = the base blob, referencesCtx occurrences 2 → 0 — turns 3 of the 37 cases in that file red, and they are the card's: the org-scoped view is absent (expected [] to include 'task_list'), the narrowness pair's org half, and the row-shape pin that reads the same row. Restoration proved by git diff HEAD empty and worktree blob back to 2ad2be625 = the HEAD blob; the file is the package's own source, read directly by vitest, so no dist sits between the mutation and the measurement.

Ledger the change moves

packages/rest/src/execctx-consumer-census.test.ts: 76 → 77 resolveExecCtx invocation sites, 97 → 98 mentions, 23 → 24 locally caught (the new site uses the continuation-line .catch(rethrowAuthzStoreUnavailable) spelling the sibling read doors use — this door does not sit behind the shared anonymous floor, so it decides an authz-store outage for itself rather than laundering it into an org-unscoped 200). Both numbers moved by +1 because the door's new comment states the memoised resolution in prose without naming the symbol; the block records that so the +1/+2 shape of the older entries is not read as a lost site.

content/docs/permissions/system-context.mdx: four rest-server.ts elevation-read anchors moved by the same +12 lines the comment added — pure line rot, rewritten by node scripts/check-system-context-census.mjs --fix, no row content changed.

Verification

Run at f90984fa1, the branch head:

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derived its own change set (5 paths) and 83 runnable families; all 83 run green. The 7 families whose argv takes a value from the workflow ($RUNNER_TEMP, ${{ matrix.shard }}, $PROVENANCE) are NOT MEASURED — there is no local invocation of them, which is the tool's own verdict, not a skip.
  • pnpm --filter @objectstack/rest test — 180 files / 3082 tests green.
  • pnpm --filter @objectstack/rest typecheck — both legs green; tsc -p tsconfig.test.json --listFiles confirms all three edited files are in the checked program, so the test layer is measured and not merely advertised.
  • pnpm lint — the whole repo, eslint . --no-inline-config, green. No narrowing claimed.
  • ⚠️ Declared narrowing: every heavy command ran through scripts/pm/os-verify-lock.sh, which reports UNLOCKED (declared) on this hostflock is util-linux and this is macOS, so the shared verify lock was never taken and nothing was serialized. Declared, not silent.

Not addressed here


Generated by Claude Code

hotlong and others added 3 commits September 5, 2026 10:40
… raw

`GET /api/v1/meta/:type/:name/references` backs the admin "Used by" panel,
whose empty case renders "Nothing in the metadata graph points at this item.
Safe to delete." to an operator about to delete something. The door named no
organization, so `findReferencesToMeta` swept the environment partition only
and an org-scoped `view` pointing at the item was invisible — a false
clearance on a destructive action (the ADR-0110 D3 harm this route's own 501
refusal exists to prevent).

The door now resolves the memoised exec ctx and passes `ctx?.tenantId` RAW.
Not pre-gated on `canonicalMetaUrlType(req.params.type)` the way the sibling
`/meta` doors are: that type is the reference TARGET, while the organization
is spent on the SOURCES, so gating on it would suppress the organization for
exactly the `object` / `flow` / `app` deletes this is about. Raw is safe
because `getMetaItems` applies `organizationIdForMetaRead` to its OWN request
type since #14683 — the per-source-type decision is already the callee's, and
`request.organizationId` has exactly one use inside `findReferencesToMeta`.

Pins in `rest-server-meta-read-org-scope.test.ts` drive real routes over a
real protocol: the org-scoped `view` is now found (red on the base commit),
and the narrowness control proves a non-overridable SOURCE is still read
env-wide with no phantom row resurrected. The `resolveExecCtx` census ledger
moves 76 → 77 sites / 97 → 98 mentions, 23 → 24 locally caught.

No new parameter, response field or contract surface: ADR-0131 D6/D7 retires
this partition in v18, so nothing is built on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s door shifted

`check:check-system-context-census --fix` output: the four `rest-server.ts`
elevation-read anchors on the system-context page moved by the same +12 lines
the door's comment added. Pure line rot, no row content changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l pin

The pin reads the refusal code through both of this route's envelopes because
the two disagree; that disagreement is now a filed card rather than an
unattributed observation in a comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 22a006b2666b5400129132eb6497e60b01fd9c39packageMentionDocs.

@hotlong
hotlong added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit e13ede8 Sep 5, 2026
35 checks passed
@hotlong
hotlong deleted the claude/issue-13753-references-door-org-forwarding branch September 5, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant