Repository navigation
fix(plugin-sharing): a share link whose redactFields names no column of the object is refused at mint - #29293
Conversation
…s no column of the object The mint stored redactFields as sent, so a misspelt entry stripped nothing at resolve while the answer still listed it as hidden. createLink now judges each entry against the column set a read serves (objectql's declaredColumnSet, newly re-exported from @objectstack/objectql/core) and answers 400 VALIDATION_FAILED naming the unknown entries, minting nothing. Claude-Session: https://claude.ai/code/session_01ADHkmPZTPQs1sThmyLtPtR Co-authored-by: Claude <noreply@anthropic.com>
…ture names it Claude-Session: https://claude.ai/code/session_01ADHkmPZTPQs1sThmyLtPtR Co-authored-by: Claude <noreply@anthropic.com>
…he engine's update/delete/limit contract Claude-Session: https://claude.ai/code/session_01ADHkmPZTPQs1sThmyLtPtR Co-authored-by: Claude <noreply@anthropic.com>
… where combinator it does not implement Claude-Session: https://claude.ai/code/session_01ADHkmPZTPQs1sThmyLtPtR Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 25 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 839bd9d94a680bf268681ceddb2b2834328b8585 && git checkout 839bd9d94a680bf268681ceddb2b2834328b8585
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 276ca307c5a1b2d9f83cd22c54edc14c1c1351c1 04c61e78e8bbf72cc36d704295c0e6670d59c87d && git checkout -B drift-repro 276ca307c5a1b2d9f83cd22c54edc14c1c1351c1 && git merge --no-ff 04c61e78e8bbf72cc36d704295c0e6670d59c87d
node scripts/docs-audit/affected-docs.mjs --json 276ca307c5a1b2d9f83cd22c54edc14c1c1351c1
|
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Contract-review-claim: The lane seat runs this PR's review in an isolated reviewer. The PR declares |
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Contract reviewServed-tier: Reviewed read-only at 2026-11-02T11:31Z: the net diff ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Findings: M1 (must-fix) — Implemented-by: VERDICT: FAIL |
…uest-shape checks; declare the mint narrowing as breaking Claude-Session: https://claude.ai/code/session_01ADHkmPZTPQs1sThmyLtPtR Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Contract-review-claim: session_01ADHkmQjPiXgNoSaEpqeGvY · head 04c61e7 · 2026-11-02T12:38Z |
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Contract reviewServed-tier: Round 2, rendered read-only at 2026-11-02T12:41Z. Supersedes record 6576071712 (FAIL, on head ① Derived judgmentsRe-read on the WHOLE net diff; the delta touches no accept set or public surface beyond moving one check earlier.
② Semver level
③ Boundary flags
④ Design judgment
Findings: none. M1 and S1 from record 6576071712 are both resolved on this head. Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY 席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #29130, 5 files. Changeset: patch on both packages, Clause-②: yes (narrowing), a BREAKING banner with FROM → TO, and one ADR-0087 marker not-required (no-migration-prescription). PR body rewritten by the seat from the dev's r2 file at 2026-11-02T12:38Z. Not governed (check-governed-merges governed: false). |
| Rule | A redactFields that is not an array of strings answers 400 VALIDATION_FAILED before any record read. A name outside the object's declared columns answers 400 after the authority 403. Omitted, null and [] store null. |
| Round 2 | M1, the declaration arm, is resolved. S1 is resolved: the shape check moved before readRecord, and a new pin checks that a malformed list causes zero engine.find calls. |
| Evidence | Dogfood VERDICT 0 (3208 passed / 0 failed), plugin-sharing 1316/1316, gates 71/71. CI on this head: 52 runs, 37 green, 15 skipped, none red. check-expected-skips OK (15, all in main's roster). |
| Prose nit, not filed | The body's 「先看」 bullet still names assertRedactFields, which is now redactFieldsShape + assertRedactFieldColumns. This is reader-only and doesn't hold the landing. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay. |
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
Requested by Steve · project thread
Fixes #29130
Clause-②: yes (narrowing)
Metadata-protocol: none
This PR widens one surface and narrows another, so it takes the
(narrowing)arm. The widening is one new export on@objectstack/objectql/core(declaredColumnSet). The narrowing is the share-link mint: aredactFieldsentry naming no column of the object was accepted201onmain, and is now refused400 VALIDATION_FAILEDwith nothing minted (measured below on base and head). The changeset carries the**BREAKING**banner with the FROM → TO fix and the ADR-0087 dispositionnot-required (no-migration-prescription). Both packages stay at patch. Contract review 6576071712, M1.维护者速读
redactFields里每个字段名都必须是该对象真实存在的列,否则 400 拒绝,不生成链接;objectql 的./core多导出一个现成函数declaredColumnSet。packages/plugins/plugin-sharing/src/share-link-service.ts(assertRedactFields和它在createLink里的位置,在权限检查之后)、packages/plugins/plugin-sharing/src/share-link-redact-fields.test.ts、packages/objectql/src/core.ts。pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-redact-fields.test.ts,预期 11 个用例全过。What changes
ShareLinkService.createLinkchecksredactFieldsbefore anything is written, in two steps. First the shape: the list must be an array of strings. That check runs with the mint's other request-shape 400s (object,recordId,emailAllowlist), before any record is read. Then the columns: each entry must name a column a read of the object serves: the object's field map plus the platform-provisioned columns (id,created_at,updated_at). Otherwise it answers400 VALIDATION_FAILED, naming every unknown entry once, and no link row is written. This is the request-shape family the mint already uses (object is required,emailAllowlist is required when audience=email); no new error code.403and learns nothing about the object's columns.declaredColumnSet's shared rule:undefinedmeans no opinion.POST /api/v1/share-links) and the dispatcher twin both callcreateLink, so both answer the refusal with the door's existing envelope. No route edit was needed.@objectstack/objectql/corere-exportsdeclaredColumnSetfromdeclared-read-columns.ts. That module says its list is the one every read and write door reads, so the mint asks it too and keeps no copy of the column list..changeset/29130-share-link-redact-fields.md(patch, both packages).Forced test edits: none. No existing test changed. One ledger row was forced: the new suite's engine double is pinned to the engine's
update/deletedispatch contract (assertEngineUpdateDispatch/assertEngineDeleteDispatch), socheck:engine-double-contractasked for its twopinnedrows inscripts/engine-double-contract.pinned.json(written with--write: 0 rows added or grown elsewhere, 0 lost). The same double holds the caller'slimitand refuses awherecombinator it does not implement, which is whatcheck:objectql-double-limitandcheck:where-matcherask of a new double.Tests
New file
packages/plugins/plugin-sharing/src/share-link-redact-fields.test.ts, 11 cases. The fixture schemas are shaped like the registry's, soidis not in the field map:find), even for a record the caller cannot see;id,created_atandupdated_atare accepted even when the field map omits them;null;403 FORBIDDENcomes first and does not echo the name;400 VALIDATION_FAILEDand stores no row; a valid entry gives201.Acceptance notes
redactFields(only create, list, revoke and resolve), so the mint is the only place the list enters the store.Verification
All readings were taken in this checkout at head
04c61e78, base104cd1a5(merge base withorigin/main).Premise on base (104cd1a)
A throwaway test minted with one misspelt name against an opted-in object. The row stored
redact_fields: ["secrte"], andresolveTokenansweredredactFields: ["secrte"]. Exit 0 (MINTED ["secrte"],RESOLVED redactFields ["secrte"]). The file was deleted afterwards.Package suites
pnpm --filter @objectstack/plugin-sharing test: Test Files 60 passed (60), Tests 1316 passed (1316).pnpm --filter @objectstack/plugin-sharing typecheck: exit 0.pnpm --filter @objectstack/objectql typecheck: exit 0 (at38ded5e5;objectqlis unchanged since).38ded5e5; the round-2 diff moves only the shape check's position). Runtime share-link domain tests (7src/domains/share-links-*.test.tsfiles plussrc/domain-handler-registry.test.ts): 8/8 files, 268/268 tests. Dogfood share-link files (share-link-combined-gates,share-links-self-list,showcase-client-liaison-fixtures,guest-grants-channel): 4/4 files, 41/41 tests.ui/:git grep redactFieldsfinds the console's resolve-answer readers and shape tests. The onlyui/test that mints (share-dialog-locale-13460.test.tsx) mocks the wire and sends noredactFields, so there was nothing to run against the service.eslint --no-inline-configon the touched source/test files): exit 0.(narrowing)arm):pnpm --filter @objectstack/dogfood teston04c61e78, under the lock, after the^...closure build. VERDICT command-exit 0; Test Files 388 passed, 3 skipped (391); Tests 3208 passed, 28 skipped (3236); no failures.Ablation (negative pins)
Put the old accept back with
scripts/ablation-replace.mjs, replacing theassertRedactFields(...)call with the rawinput.redactFields ?? []. The anchor went from 1 to 0 hits and the blob from 7db89b579f0c to efb2844eef05. Result: 4 failed and 6 passed (10, at38ded5e5, before the round-2 shape pin was added). The red cases were the three refusal pins and the REST 400 pin; the valid-mint, provisioned-column, empty-list, no-field-map and 403-ordering cases stayed green, as they should. Restore: the blob matched HEAD again (7db89b579f0c) andgit diff HEADwas empty. The test imports the service fromsrc/, so nodistleg was needed.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat04c61e78derived 71 commands. All 71 ran on this head and every one exited 0.--ranwith exit codes recorded:71 derived famil(ies) accounted for — 71 run, 0 NOT-MEASURED (a DERIVED zero — all 71 recorded an exit code and none of them is 3). This includescheck-adr-0087-registration --base origin/main(exit 0):1 declared-breaking changeset(s), each carrying an ADR-0087 disposition—[BREAKING+clause-②-narrowing] not-required (no-migration-prescription). It also includescheck-changeset-no-majorandcheck-empty-changeset, both exit 0.An earlier run on
e57ba0d3found three real reds, all about the new suite's engine double:check:engine-double-contract(update/delete not dispatch-pinned),check:objectql-double-limit(afindthat ignoredlimit) andcheck:where-matcher(a matcher that would read a combinator as a field name). Each was fixed in the double, as the gate prescribes; no baseline grew. Two prerequisite exits from that run were cleared before the final run: the teardown-shape self-test's pinned fixture commit was fetched, and the seven unbuiltdist/trees thatcheck:dual-build-cjs-loadsreads were built.Generated by Claude Code