Repository navigation
feat(lint)!: report a searchableFields entry that names a secret or password field - #27811
objectstack-fleet[bot] merged 5 commits into
Conversation
…ssword field The engine withholds every credential column from the set a search scans (ADR-0100), an author-declared one included, without a word. The searchable-field-unsearchable rule now reports such an entry at error on every surface it covers, judged by a new spec predicate isCredentialFieldType that objectql's collectCredentialFields reads too. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…alFieldType Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…the changeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…ragment Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
📓 Docs Drift Check4 anchor(s) derived from 3 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 169 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 9e2b153acba06213778a47ec816ec50b937cb117 && git checkout 9e2b153acba06213778a47ec816ec50b937cb117
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ca167f6d23417aaa85b1b3b5fa2e30286b3c4350 257e83df2f0883966cf37254eae5eab87d36bee4 && git checkout -B drift-repro ca167f6d23417aaa85b1b3b5fa2e30286b3c4350 && git merge --no-ff 257e83df2f0883966cf37254eae5eab87d36bee4
node scripts/docs-audit/affected-docs.mjs --json ca167f6d23417aaa85b1b3b5fa2e30286b3c4350 |
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V 席位核对 · ACCEPT (lane part) · card #27705 · 2026-10-30T00:27Z
Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-29T17:05Z) · head 257e83d |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract-review-claim: Generated by Claude Code |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract reviewServed-tier: Inputs read: card #27705 (body, triage 6506902320, claim 6509648663, os-dev-report 6511812214), PR #27811 (body, 11-file list, net diff from the files endpoint, comments 6511803328 / 6511839038 / 6511932528) and the check-runs on this head. The cited code was read in the committed tree at this head; nothing was built or run. The remainder of the diff (the lint rule, its tests, ① Derived judgments
② Semver level
③ Boundary flagsDev deviations (os-dev-report 6511812214), each answered:
Note on provenance, not a finding: the hunks cite "ADR-0100 search withhold"; ADR-0100 decides the read mask and the better-auth exemption and names the aggregate gap as a follow-up, and does not itself decide the search withhold, which the engine comment at ④ Design judgment
Holds:
Tried to break it:
Verdict on the schema hit: the predicate holds against the ruling it implements and against both first consumers; the one should-fix above is a missing pin, not a wrong answer. Gates on this head: read from the check-runs on Findings:
Implemented-by: VERDICT: PASS Generated by Claude Code |
Requested by 田騏 · project thread
Fixes #27705
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up #27660
维护者速读
os validate、os build和元数据发布门现在把searchableFields里指向secret/password字段的条目报为错误(沿用规则searchable-field-unsearchable),对象自身集合、列表视图、React 页面 ListView 组件的searchableFields属性 全覆盖。formula条目一致。packages/lint/src/validate-searchable-fields.ts(新增的凭据检查段,位于 formula 检查之后、可接受性检查之前)、packages/spec/src/data/masked-field-types.ts(新谓词isCredentialFieldType)、packages/objectql/src/secret-fields.ts(一行替换,行为不变)。pnpm --filter @objectstack/lint exec vitest run src/validate-searchable-fields.test.ts,预期全部通过,其中包括 "a secret or password entry" 这一组。Independent review: none run (quota); the PR waits for the seat's contract-review hand-off.
What changes
packages/lint/src/validate-searchable-fields.ts. A credential check sits beside theformulacheck from A virtual formula field declared in searchableFields clears lint and the #4254 gate, then silently matches nothing — the same fail-open shape #4254 closed one axis over #6674. It reports asearchableFieldsentry whose field type issecretorpasswordundersearchable-field-unsearchableaterror. It runs on every surface the virtual check covers: the object's own set, an object's built-in list views,defineViewlists and named list views, a flattened list overlay, a ViewItem record, and a react page's ListViewsearchableFieldsprop (through the sharedcheckSearchableFieldListcore). The remedy line isDrop "NAME": credential fields are never searched. The module note (check 4) gives the reason for the severity. The check runs before the compiler/validate: view searchableFields not validated against searchable field types — a declaration typo 400s the whole list at runtime #4830 admissibility pass, so a view over an auto-default object gets this verdict instead of the generic "of type 'password'… target a text-like field".packages/spec/src/data/masked-field-types.ts. AddsisCredentialFieldType(fieldType). It answers whether a type is masked on read on at least one object, which today meanssecretandpassword. It reads that from the keys ofMASKED_ON_READ_FIELD_TYPES, so it adds no second type literal, and it ignores the better-auth exemption.isMaskedOnReadFieldTypenow uses it for its own-key test, and its answers are unchanged.packages/objectql/src/secret-fields.ts.collectCredentialFieldsnow readsisCredentialFieldTypeinstead of its owndef.type === 'secret' || def.type === 'password'literal. This is the one-line swap the order allows. The linter and the engine's search withhold (expandSearchOnAst, unchanged) now judge credential types from one predicate.packages/objectql/src/engine.tsis not touched.packages/lint/src/rule-explanations.ts.os explain searchable-field-unsearchablegains a paragraph on what the engine does with a credential entry on each surface, why there is no exemption, and why the finding is an error.packages/spec/api-surface/data.json,packages/spec/export-origins/data.json. Regenerated bycheck:generated --fix(one line each, the new export)..changeset/27705-searchable-credential-fields.md.@objectstack/lintminor,@objectstack/specand@objectstack/objectqlpatch. It carries both declaration lines and an ADR-0087not-required (no-migration-prescription)disposition, the precedent of27225-mail-formatter-shape-lint.md.Tests
validate-searchable-fields.test.ts:secretandpasswordon the object's own set, and on a list view narrowing (it.each). Also a view entry the object itself declares (both findings), a better-authpassword(exemption-free), a control where text and email entries in the same declarations stay clean, and theos explaintext. Thefix:-line coverage control adds the credential arm.validate-react-page-props.test.ts: a ListViewsearchableFields={['name', 'api_key', 'portal_password']}prop reports both credential entries aterror.runtime-gate.object-writes.test.ts: the object publish door refusessearchableFields: ['owner', 'api_key'](api_keyissecret).masked-field-types.test.ts:isCredentialFieldTypeover everyFieldType, its agreement with "masked on some object", the one cell where it andisMaskedOnReadFieldTypediffer (password× better-auth), and unvalidated input.Acceptance notes
error, the severity the triage ruling sets (the same as the A virtual formula field declared in searchableFields clears lint and the #4254 gate, then silently matches nothing — the same fail-open shape #4254 closed one axis over #6674formulaentry). HenceClause-②: no (narrowing).packages/spec.SEARCH_AUTO_EXCLUDED_TYPESalso listsjson,lookupand other types an author may declare, andisMaskedOnReadFieldTypeapplies the better-auth exemption that the search withhold does not apply. So the predicate was added next to the mask table it reads.assertSearchFieldsAreSearchable) admits a$searchFieldsentry naming a credential field that the object itself declares, and the engine then withholds it. ReadingresolveSearchFields, a request naming only such fields would be answered over the object's whole allowed set. That is a narrower request answered more widely. This is a reading of the code, not a measurement through a public door, so it is left here for the seat. It sits outside this card's "no runtime change" scope.searchableFields should not list itinskills/objectstack-data. The boundary table inskills/objectstack-ui/rules/list-views.md, which lists eachsearchable-field-unsearchablerow, has no credential row. skills(objectstack-data): teach that a secret or password field is never a filter, sort or search key, only a has-a-value check (Skills-docs follow-up owed by #27477) #27660 could take that row as well. This PR touches noskills/**file.Verification
Ablation: new pins fail without the fix
node scripts/ablation-replace.mjs --file packages/lint/src/validate-searchable-fields.ts --anchor " if (isCredentialFieldType(ctype)) {" --replacement " if (false && isCredentialFieldType(ctype)) {" -- …vitest run src/validate-searchable-fields.test.ts src/validate-react-page-props.test.ts1343dd89e129to892e53b49026Tests 8 failed | 241 passed (249): the 7 credential pins plus thefix:-line coverage control. The text-field control and theos explainpin stayed green, as expected.1343dd89e129),git diff HEADemptyPackage tests and typecheck
@objectstack/linttest:Test Files 178 passed (178),Tests 7633 passed (7633). typecheck:tsc --noEmitclean,check:test-typecheck: OK.@objectstack/spectest:Test Files 755 passed (755),Tests 22452 passed | 1 todo. typecheck exit 0.@objectstack/objectqltest (--project local):Test Files 490 passed (490),Tests 9244 passed (9244). typecheck:check:test-typecheck: OK.@objectstack/spec check:generated: 2 artifacts stale before regeneration (api-surface,export-origins) and regenerated with--fix.Gates (dispatch-gates --commands, 85 families, at 257e83d)
All 85 exit 0 at head
257e83df2f, each exit code captured before any pipe.--ranreconciliation: 85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN. A first run at340955697ahadpnpm check:dual-build-cjs-loadsat exit 3 (PREREQUISITE NOT MET, no full build yet). It was re-run in the final union after the dogfood closure build: 112 require entry points across 69 packages load.check-adr-0087-registrationfound one declared-breaking changeset, and it carries its disposition. The level axis ofcheck-changeset-no-majoris scoped to the PR and reads this body on CI.Dogfood
At
257e83df2f, after the@objectstack/dogfood^...closure build, under the lock:pnpm --filter @objectstack/dogfood test, VERDICT command-exit 0,Test Files 355 passed | 3 skipped (358),Tests 2867 passed | 20 skipped (2887). No shipped metadata names a credential field insearchableFields.🤖 Generated with Claude Code