Repository navigation
fix(core)!: an import reads which fields are references through the spec's arbiter, so a user field without reference resolves against sys_user (#22785) - #22818
Conversation
…ough the spec arbiter, so a user field without reference targets sys_user Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…mplate doors, with Field.user and the unserved-target refusal as controls Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…eld targets through the spec arbiter Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…port-user-field-target
…port-user-field-target
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e61ab563e919cc90e863ee2c1941a66389dc0eda && git checkout e61ab563e919cc90e863ee2c1941a66389dc0eda
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 27f0d83fb83bab61398ead3e8bbc0f529f0ee9fb 7fffd20e8449fa6adb7591678f403330e76ee8c2 && git checkout -B drift-repro 27f0d83fb83bab61398ead3e8bbc0f529f0ee9fb && git merge --no-ff 7fffd20e8449fa6adb7591678f403330e76ee8c2
node scripts/docs-audit/affected-docs.mjs --json 27f0d83fb83bab61398ead3e8bbc0f529f0ee9fb
|
…, with the BREAKING banner and its ADR-0087 disposition Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…t as a migration mapping Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs, and nothing else: card #22785 (body; comments 6106426530 triage, 6106696904 claim, 6107672102 round-1 dev report, 6107745753 round-2 dev report), the precedent 6106439614 (#22739's contract review on PR #22770), PR #22818 (title, body, its 6-file list, its 7-commit list, and the net diff from the merge-base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #22785
Clause-②: yes (narrowing)
What changes
buildFieldMetaMap(packages/core/src/utils/import-field-meta.ts) now asks the spec's arbiterreferenceTargetOfboth WHETHER a field is a reference and WHAT its target is. Before, only a field carrying areferencestring reached the arbiter. So auserfield written withoutreferencewas not a reference at the import, export and import-template doors, although the arbiter (and the data door's$expand) reads it assys_user.IMPLICIT_REFERENCE_TARGETSis the only one.referencekey that is present but not a string still stays out of the arbiter, which throws on it. Such a field names no target, as before.referencestill names no target.servesReferenceTarget, security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739). No code path bypasses it.packages/rest, inimport-runner.ts(toFailedResultuntouched) or inpackages/types.Door readings, before and after
Real boot through the dogfood harness (SecurityPlugin, ObjectQL, SQL driver, REST). Base
9f5eca52b3against the fixce23f40ae8, with@objectstack/corerebuilt and itsdistchecked. The fixture object hasownerwritten as{ type: 'user' }(noreference) andassigneewritten asField.user(). The administrator and the member answered identically, except on the hidden-user row.POST /data/:object/importand/import/jobsowner= a user's emailreference_not_found("no sys_user record has id ..."), nothing storedassignee(Field.user) = the same emailowner= a visible user's idowner= an unknown emailreference_not_found(the write's sentence)reference_not_found("no record matches ...")owner= the id of a user outside the member's organizations (the member'sGET /data/sys_user/IDanswers 404)reference_not_found; CONTROLassigneeanswersreference_not_foundin bothGET /data/:object/export(JSON and CSV)ownerholding the admin's idassigneeGET /data/:object/export?template=trueownerinstructions rowpullConnectorSource, stub protocol, built core)owner_emailmapped toownerreference_not_foundsys_user's served map (26 fields)manager_idtosys_user,primary_business_unit_idtosys_business_unit; 0 reference-typed fields withoutreferencePOST /data/:object(not this change)owner= the hidden user's idField.userfield already gives.Pins
import-runner-reference-exposure.test.ts: the security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739 control block (ownerwith no target) is replaced by four cases.sys_userfor both user fields, and no target for a non-reference type or for a non-string carrier.referencestores the user's id.Field.userdoes the same.sys_user(apiEnabled: false) refuses both cells, andfindDatais never called.export-format.test.ts:referenceFieldNamesincludes the field withoutreference.import-template.test.ts: its instructions nameUser.import-user-field-target.dogfood.test.ts: the sync and jobs doors forownerand for theField.usercontrol, plus the export door.Ablations
Each ablation ran on a committed tree through
scripts/ablation-replace.mjs(WRAP mode). Each restore was proven: blob equals HEAD andgit diff HEADis empty.f.reference == null ||). Core rebuilt;ablation-dist-preflight.mjs --absentgreen over 14 built files. Before the mutation, the fixed build carried the marker indist/index.js(count 1).Field.usercases and the 13 security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739 pins stay green.Field.userlegs stay green.if (!(await served)) return {};inimport-runner.ts): 10 red / 7 green. The red cases are the 9 security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739 withheld legs and the new unserved-sys_usercontrol, so that control is security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739's refusal.(referenceTargetOf(f))): 4 red / 13 green. Each red is the arbiter's TypeError on the non-string carrier.Verification
The package suites ran at
4b78f72c5f. The next merge, to272f15bd62(origin/main098481744f), brought onlyplugin-webhooks, the lockfile andscripts/engine-double-contract.pinned.json. The dogfood pins were re-run at272f15bd62.test93 files / 2352 passed;test:repo5 / 55, the enumeration pinsecond-object-read-exposure.pin.test.tsincluded;typecheckexit 0 (check:test-typecheckOK).typecheckexit 0.admin-import-users2 files / 50. service-automation:connector-pull3 files / 22.typecheckexit 0. The two pins (this one and security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739'simport-reference-exposure) passed 16 / 16 at272f15bd62.--listFiles).272f15bd62.dispatch-gates --commandsderived 68 gates; the dispatch's 50 are a subset. All 68 ran, and each exited 0.check:dual-build-cjs-loadsfirst answered PREREQUISITE NOT MET (8 packages had nodist). After those packages were built it exited 0.--ranreconcile: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN.272f15bd62.eslint --no-inline-configover the 5 changed.tsfiles.--print-config: 5 to 6 active rules each, none ignored.--format json: 5 files, 0 errors, 0 warnings.eslint.config.mjsenables no type-aware linting, so no untouched file's verdict can move. Repo-wide lint belongs to CI.7fffd20e84, no code change).check-adr-0087-registration --base origin/main0 ([BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription));check-changeset-no-major --base origin/main0;check-empty-changeset --base origin/main0;check:changeset-gate-self-tests0;check:nul-bytes,check:issue-citations(both spellings),check:doc-authoring,check:pm-changeset-deadline-census,check:objectui-changeset0. Re-deriveddispatch-gates --commands: the same 68 gates as above.no-migration-prescription. The bullets are unchanged.origin/mainhas since moved to23419bafb7. Its one file overlapping this branch's reads, the enumeration pin, changes a different row (the relation-filter one).git merge-treeis clean, so the merge is left to CI and the queue.Acceptance notes
yes (narrowing): the hidden-user id moves from stored to refused, and the export column moves from the stored id to the user's name. The changeset therefore carriesfix(core)!, the**BREAKING**banner naming both populations, and the ADR-0087 markernot-required (no-migration-prescription). It also adds a one-line fix for a reader that needs the stored id: the record read (GET /api/v1/data/:object, withoutexpand).buildFieldMetaMapserves the export and template doors frompackages/rest, so their answers moved with nopackages/restsource change. They are pinned by test only.Generated by Claude Code