Skip to content

fix(metadata-protocol)!: refuse a $searchFields entry naming a secret / password field at the data door - #27936

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-27829-searchfields-credential-refused
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-27829-searchfields-credential-refused

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Requested by Sales · project thread

Fixes #27829

Clause-②: no (narrowing) — the data doors (GET /data/:object, POST /data/:object/query, and every other caller of findData) refuse a search-field override naming a secret / password field, which they answered 200 over the rest of the declared search set before. Nothing is widened.
Metadata-protocol: none
Skills-docs: none — no skill teaches or emits a credential name in searchFields (git grep -n "searchFields\|searchableFields" -- skills at this head: no hit mentions a secret / password / credential field); the one docs page that lists the search refusals gains the row here (content/docs/api/data-api.mdx).

维护者速读

  • 改了什么:对象的 searchableFields 声明了 secret / password 字段时,请求里 $searchFields(或 POST 的 searchFields / 对象形 search.fields)点名该字段,数据接口现在回 400 INVALID_FIELD 并点名字段,不再读库。
  • 为什么改:以前门放行这个名字,引擎又按凭据规则把该列从搜索里拿掉,结果悄悄搜了声明里的其余字段:只想搜 api_key,拿到的是按 name 匹配的行,看不出任何异常(不泄露凭据,但答非所问)。
  • 风险与回滚:只影响在 $searchFields 里点名凭据字段的调用方;Console 列表搜索会原样回传视图的 searchableFields,若某视图声明了凭据字段,搜索会从 200 变 400(lint 早已把这种声明报为错误)。引擎侧的凭据隐藏不动,仍是兜底。回滚 = revert 本 PR。
  • 先看:packages/metadata-protocol/src/protocol-query-validation.ts(assertSearchFieldsAreSearchable 新增的 credential 一支与它的文案),packages/rest/src/data-search-fields-credential-door.test.ts(门级钉子)。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/data-search-fields-credential-door.test.ts,预期 6 个用例全过(secret、password 各三:GET 拒收、POST 拒收、对照组照旧)。
  • 需要你做的:无,席位落地(进队前欠一次契约复核)。

What changes

  • assertSearchFieldsAreSearchable (metadata-protocol/src/protocol-query-validation.ts): after the allowed set is resolved, a requested name the set KEPT whose field type is a credential (isCredentialFieldType, @objectstack/spec/data, the predicate the engine's collectCredentialFields withhold reads) is refused 400 INVALID_FIELD with field, fields, object and param set, in the envelope every other refusal of this gate uses. Message: Field 'api_key' on object 'OBJ' is type 'secret', a credential, and cannot be searched. …, the wording family the auto-default path already gives a credential name ("is type 'secret'"), plus the remedy: remove it from searchableFields when the object declares it, search another column. Only a declared searchableFields keeps a credential in the allowed set (the auto default excludes the types and still refuses them with its own message, unchanged).
  • It covers the standalone slot in every spelling and the object-form search.fields, because protocol.ts hands both to this one gate (measured: the POST legs below). No line of the hot file protocol.ts moves.
  • Order in the cascade: unknown → stale declaration → virtual → credential → unsearchable, so a request with a typo still gets the typo answer first.
  • The engine is unchanged: its credential withhold (ADR-0100) stays as the fail-closed floor for direct engine callers.
  • New door pin rest/src/data-search-fields-credential-door.test.ts: real RestServer → findData → ObjectQL → sqlite, the data: a blank or comma-only $searchFields is read as absent, so a search the caller narrowed runs over the default search fields instead of being refused #27484 harness; two objects declaring searchableFields: [name, api_key] with api_key secret and password. GET $searchFields=api_key and $searchFields=name,api_key, POST searchFields: ['api_key'] and object-form search.fields: ['name', 'api_key'] each answer 400 INVALID_FIELD naming api_key and its type, with zero reads of the object; controls $searchFields=name, no $searchFields, and POST searchFields: ['name'] answer 200 with the two name matches.
  • Docs: one row in the refusal table of content/docs/api/data-api.mdx.
  • Changeset .changeset/27829-search-fields-credential-refused.md (patch on @objectstack/metadata-protocol, BREAKING banner, ADR-0087 not-required).

Tests

Head f6a224d1 (merge of origin/main d770b150 onto the fix commit 1d6b7d0d). Every reading below is from this head unless it says otherwise.

Red first, then green (door pin)
  • Before the fix (the new test against the gate at the base 637cfc67): data-search-fields-credential-door.test.ts 2 failed / 2 passed of 4 — $searchFields=api_key answered 200 with qq7-a, qq7-b (matched in name), for both secret and password: the card's reading.
  • After: 6/6 (the POST legs were added with the fix). At this head: data-search-fields-credential-door.test.ts + data-empty-search-fields-door.test.ts 11/11.
Package suites and typecheck
  • pnpm --filter @objectstack/metadata-protocol test :: exit 0 · Test Files 282 passed, 4 skipped (286) · Tests 21843 passed, 26 skipped (this head; under os-verify-lock.sh, VERDICT command-exit 0)
  • pnpm --filter @objectstack/metadata-protocol typecheck, pnpm --filter @objectstack/rest typecheck :: exit 0 (this head)
  • pnpm --filter @objectstack/rest test :: exit 0 · Test Files 348 passed (348) · Tests 6746 passed, 332 skipped (on 1d6b7d0d; the merge brought no rest change)
  • eslint --no-inline-config over packages/metadata-protocol and packages/rest :: exit 0
Ablation (red, then restored to the HEAD blob)
  • node scripts/ablation-replace.mjs --file packages/metadata-protocol/src/protocol-query-validation.ts --anchor ": credential.length > 0 ? [credential, 'credential' as const]" --replacement ": (globalThis as any).ABLATE_27829 === 'refuse' && credential.length > 0 ? [credential, 'credential' as const]" — anchor 1 → 0, blob 3028c46e → 774faa00; rebuilt; ablation-dist-preflight.mjs @objectstack/metadata-protocol ABLATE_27829 hit in dist/index.js and dist/index.cjs.
  • Red: 4 failed / 2 passed of 6 — all four refusal legs (GET and POST, secret and password) answered 200 with the name matches; the two control legs stayed green.
  • Restored: blob after restore == HEAD 3028c46e, git diff HEAD empty; rebuilt; preflight --absent clean over 24 built files, tree clean; door test 6/6 again.
Dogfood battery (this head)

pnpm exec turbo run build --concurrency=2 --filter='./packages/**' (76/76) then pnpm --filter @objectstack/dogfood test under os-verify-lock.sh, no shard, no path: VERDICT command-exit 0 · Test Files 357 passed, 3 skipped (360) · Tests 2875 passed, 20 skipped (2895) · no failures.

An earlier run on c55d2e92 (an older merge of main) had 4 failures in 3 files (error-sentences-live ×2, object-hook-lifecycle-scratch, platform-app-object-entry-views), none search-related; main fixed them in #27878 and #27873, and this head includes both.

Verification

Local subset, head f6a224d1: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 90 families; every one run with its exit code captured before any pipe, all 90 exit 0 (check:type-check-debt under os-verify-lock.sh). --ran reconcile: "90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero: every entry carries its exit code). On the first pass (1d6b7d0d) spec check:skill-examples and check:dual-build-cjs-loads answered exit 3 (PREREQUISITE NOT MET, dists unbuilt); both are exit 0 here after the full package build, not read as a verdict before. CI-run families: see this PR's check-runs.

Acceptance notes

  • Producer that will see the new 400: the Console's list search (ui/packages/plugin-list/src/ListView.tsx, ui/packages/plugin-grid/src/objectGridRowLoad.ts) sends the VIEW's searchableFields as $searchFields verbatim. A view declaring a credential entry used to search the rest of its list silently; it now answers 400 naming the field. validate-searchable-fields (@objectstack/lint) already reports that declaration as an error on every surface, so the fix for the author is the one the lint names.
  • The declared branch of the existing "not searchable" message lists the allowed set (allowed.join(', ')), which still names a declared credential entry. Cosmetic, not changed here.
  • In-flight intersection on the hot file: at branch time the only open PR touching metadata-protocol/src/protocol.ts was fix(metadata-protocol)!: a datasource restored or published from a stored body may name only the credentials reference its row holds now #27860 (head fd02314b); git merge-tree --write-tree of this branch against it was clean (exit 0), and it has since merged into main, which this head includes. No open PR touches the other changed files.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F

… / password field at the data door

The door resolved the allowed search set from the object's declared
searchableFields, which kept a credential entry, so the request passed
and the engine's credential withhold (ADR-0100) then searched the rest
of the declared set: a narrower search answered with a wider one. The
door now refuses such an entry 400 INVALID_FIELD, judged by
isCredentialFieldType, the predicate the engine's withhold reads. The
engine's withhold stays as the fail-closed floor.

Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 2 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-api.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))
  • content/docs/api/error-catalog.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))
  • content/docs/data-modeling/field-types.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))
  • content/docs/data-modeling/queries.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))
  • content/docs/data-modeling/schema-design.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))
  • content/docs/frontend/building-crud-app.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))
  • content/docs/frontend/plugins/plugin-grid.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))
  • content/docs/frontend/plugins/plugin-view.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))
  • content/docs/protocol/objectql/query-syntax.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))
  • content/docs/ui/views.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))
  • content/docs/releases/v17/17-1.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))
  • content/docs/releases/v17/17-4.mdx (via searchableFields (literal, a string literal in assertSearchFieldsAreSearchable))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 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 ed0e71db94a73acc02512ff2ee0655106d02e585 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 504e4860f6827a090b1ccbcedbc12cbffaea0125 — the merge of head f6a224d16daa1c9f5448edbee491053a73c7d1ec into base ed0e71db94a73acc02512ff2ee0655106d02e585, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 504e4860f6827a090b1ccbcedbc12cbffaea0125 && git checkout 504e4860f6827a090b1ccbcedbc12cbffaea0125
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ed0e71db94a73acc02512ff2ee0655106d02e585 f6a224d16daa1c9f5448edbee491053a73c7d1ec && git checkout -B drift-repro ed0e71db94a73acc02512ff2ee0655106d02e585 && git merge --no-ff f6a224d16daa1c9f5448edbee491053a73c7d1ec

node scripts/docs-audit/affected-docs.mjs --json ed0e71db94a73acc02512ff2ee0655106d02e585

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ed0e71db94a73acc02512ff2ee0655106d02e585 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract-review-claim: session_01L6bhgUCizoYWQVJWjhdR6F · head f6a224d · 2026-10-30T04:37Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f6a224d16daa1c9f5448edbee491053a73c7d1ec
Local-runs: none
Scope: packages/metadata-protocol/src/protocol-query-validation.ts, packages/rest/src/data-search-fields-credential-door.test.ts, content/docs/api/data-api.mdx, .changeset/27829-search-fields-credential-refused.md + packages/spec/src/data/masked-field-types.ts, packages/spec/src/data/search-fields.ts, packages/spec/src/data/field.zod.ts, packages/objectql/src/secret-fields.ts, packages/objectql/src/engine.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol-record-reads.ts, packages/rest/src/data-action-routes.ts, packages/rest/src/data-empty-search-fields-door.test.ts, packages/lint/src/validate-searchable-fields.ts, packages/lint/src/rule-explanations.ts, packages/mcp/src, ui/packages/plugin-list/src/ListView.tsx, ui/packages/plugin-grid/src/objectGridRowLoad.ts, ui/packages/plugin-view/src/ObjectView.tsx, ui/packages/app-shell/src/views/objectViewListRenderer.tsx, docs/adr/0100-credential-field-channels.md, content/docs/api/error-catalog.mdx

Read-only review of the net diff origin/main...pr27936 (4 files, +198/−1) against the cited code at this head, card #27829 (body, triage 6513030226, claim 6513283306 at 2026-10-30T02:14Z, os-dev-report 6515056445 at 2026-10-30T04:34Z), the PR body and its Contract-review-claim 6515094829 (2026-10-30T04:37Z), and the head's check-runs, read at 2026-10-30T04:50Z. Nothing built, run or re-run.

① Derived judgments

  • Premise re-derived, holds. On main the gate resolves allowed with resolveSearchFieldResolution over the unfiltered gate.fields, so a declared secret / password entry stays in the allowed set and the request passes; the engine's expandSearchOnAst (engine.ts, the ADR-0100 block before expandOptions) builds scanFields minus collectInternalReadFields ∪ collectCredentialFields, so resolveSearchFields validates the requested credential to an empty list and, by its documented tolerance, returns the rest of the declared set — the card's "narrower asked, wider answered". The fix refuses the name at the door where the allowed set KEPT it; the engine is untouched as the floor.
  • Predicate parity (point 1): exact. isCredentialFieldType (spec masked-field-types.ts) answers from the own keys of MASKED_ON_READ_FIELD_TYPES, which are secret and password; collectCredentialFields (objectql secret-fields.ts) calls that same function per field, and so does the lint's credential check (validate-searchable-fields.ts). The FieldType enum (spec field.zod.ts) carries no other credential-like member — no token or encrypted type exists (the SEARCH_AUTO_EXCLUDED_TYPES contains 4 members not in the FieldType enum — more dead vocabulary in search-fields.ts #13716 note in search-fields.ts records that 'encrypted' never did). Both sides are exemption-free (a better-auth password is withheld and refused alike). No credential the engine withholds now passes the gate; nothing non-credential is newly refused. The flag-keyed half (internal: true) was already refused first (security(data): the data door's filter and group-by positions do not honour a field's internal: true the way its row read does — detail withheld pending maintainer #22646) by the cross-package twin of the engine's reader.
  • Reach (point 2): one gate, no bypass found. The call sits in findData's normalizer (protocol.ts, the REST 读路径:searchFields / groupBy / aggregations 指向不存在的字段时被静默降级(#4226 收口后剩下的三条轴) #4254 block): options.searchFields when present — the $searchFields alias is lowered to it above, string and array alike — else the object-form search.fields. Doors that pass a caller override through findData: GET /data/:object and POST /data/:object/query (crud-routes), the export route (data-action-routes.ts builds searchFields and hands it to p.findData); the other findData callers (import-runner, connector-pull, action-execution, admin-import-users) carry no search override. GET /api/v1/search (searchAll, protocol-record-reads.ts) sends a SERVER-derived list to engine.find — ADR-0061 Tier 1, the client sends only the term — so there is no caller narrowing to refuse there. packages/mcp carries no search override. Hooks and flows reach the engine directly, where the tolerant intersection is the documented posture for callers that pass no ingress. The count leg reuses the post-gate options.searchFields, so total and the page agree.
  • Cascade and envelope. credential is a subset of allowedSet; unknown, staleDeclared, virtual, unsearchable are subsets of its complement — disjoint, so the order only chooses the leading message: typo-class answers first, credential before the generic unsearchable. The thrown error carries code INVALID_FIELD, status 400, field, fields, object, param — the tail every reason of this gate uses. The runtime string carries no tracker number.
  • Console producers (point 3): only a lint-errored declaration moves. ListView.tsx (the $searchFields spread in the list fetch) and objectGridRowLoad.ts (the params.$searchFields assignment) send schema.searchableFields of the VIEW node; the fallbacks in ObjectView.tsx (currentNamedViewConfig ?? activeView ?? schema, where schema is ObjectViewSchema) and objectViewListRenderer.tsx (viewDef ?? listSchema) are view-level too. objectResolvesSearch in ListView.tsx reads the OBJECT's declaration only to decide whether to draw the box and sends nothing from it. Hard-coded sets in app-shell (builtinComponents.tsx, metadata-admin anchors.ts) name no credential. So the one producer that goes 200 → 400 is a view declaring a credential entry, which validate-searchable-fields already reports as error on every surface, object and view. The acceptance note holds; the dogfood battery at this head is green, and the one platform object I read with a long declared set (sys_approval_request) names no credential.
  • The test (point 5). Real RestServer → findData → ObjectQL → SqlDriver (sqlite), both credential types, GET and POST, standalone and object-form slots, plus three controls per type. The refusal legs assert the NEW branch's own sentence (Field 'api_key' on object '…' is type '…'), which the auto-default's "is not searchable" wording cannot satisfy, so a regression to the old path fails on the message as well as the status. engine.find reaches driver.find (engine.ts, the hook-wrapped read), so the wrapped counter is live; the file does not pin a positive increment — same shape as its sibling data-empty-search-fields-door.test.ts (should-fix in ④). The ablation on record gated the one new cascade arm behind a global: four refusal legs answered 200, controls stayed green, HEAD blob restored, --absent preflight clean — it isolates the new arm as the carrier.
  • Dev report cross-checks. dogfood_battery names f6a224d1, which is this head (merge of origin/main d770b150 onto the fix 1d6b7d0d). Deviation (2) verified: git log 1d6b7d0d..f6a224d1 -- packages/rest/src is empty, so the two merges brought no rest change; fix(metadata-protocol)!: a datasource restored or published from a stored body may name only the credentials reference its row holds now #27860 (7591c603) touched protocol.ts by 13 lines in the datasource credentials-ref block, not the search block, and the metadata-protocol suite was re-run at this head per the report. open_questions: [].
  • Check-runs on this head at 2026-10-30T04:50Z: 32 success, 14 skipped, 6 in progress (Test Core 2/8–6/8, Type Check · workspace), 0 failed. Green already: Lint & Repo Gates, Check Changeset, PR-body guards, Governed Surface Queue Guard, Spec property liveness, Type Check · source gates / consumer gates / debt ledger, Build Core, Test Core 1/8, 7/8, 8/8, Build Docs, Live E2E UI, Docs drift flag. The running ones are the gate verdicts for what they cover; this record does not stand in for them.

② Semver level

  • @objectstack/metadata-protocol patch, BREAKING banner, ! title, line-initial Clause-②: no (narrowing) on PR body and changeset, Metadata-protocol: none — right. What a metadata author can write is unchanged (the searchableFields schema still admits the entry; the lint already errors on it); the narrowing is on a QUERY value at the door. Post-Task Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3: a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition. The marker not-required (no-migration-prescription) is the documented category and fits: no key, export or type added, removed or re-shaped; the remedy is in the refusal. Check Changeset green.
  • @objectstack/rest: only a test file, nothing publishes — correctly absent. content/docs and .changeset are not packages.
  • Skills-docs: none verified by grep at this head: no skill names a credential beside searchFields / searchableFields.
  • Tier: the file list carries no governed path and no published schema; the changeset's (narrowing) arm decides the face, so CONTRACT_REVIEW_TIER is the tier owed and served.

③ Boundary flags

  • Scope kept as the card asked: no packages/spec/src/** hunk, no engine hunk, no line of protocol.ts. The hot-file intersection the triage named (fix(metadata-protocol)!: a datasource restored or published from a stored body may name only the credentials reference its row holds now #27860) is merged into this head.
  • Dev deviations, both acceptable: (1) a second main merge after four dogfood failures main fixed in test(dogfood): flip the three hook-abort pins to the ruled HOOK_ABORTED wire #27878 / test(dogfood): boot PlatformObjectsPlugin in the Setup/Account entry-view roster, so sys_exchange_rate resolves #27873 — the final battery is on the final head, which is what discipline §10 asks; (2) the full rest suite only on 1d6b7d0d — verified no rest change arrived with the merges, and the CI Test Core shards cover it.
  • Pre-existing, not this PR's defects; noted for the lane, no card filed by this review: (a) the declared branch's "not searchable" message lists allowed.join(', '), which still names a declared credential (the dev's own note; should-fix in ④); (b) searchAll mirrors the internal-field withhold into its skip judgement but not the credential one, so a credential-only declaration is not skipped and reaches the engine, which falls to the auto default — the lint-errored degenerate case, unchanged here; (c) lint rule-explanations.ts SEARCH_FIELDS_ECHO says the ingress "refuses an entry outside the object's allowed set" — now also a declared credential inside it; error-catalog.mdx describes the searchFields reasons at a grain that names neither virtual nor credential, consistent with before. The docs-drift list was read; data-api.mdx is the one page that enumerates the refusals, and it gained its row.
  • Who sees the 400: an API caller echoing an object's lint-errored declaration, and the Console on a view with such a declaration. This is the narrowing the triage ruled. Rollback is a revert.

④ Design judgment

  • packages/metadata-protocol/src/protocol-query-validation.ts — holds against the code it cites: one predicate shared by gate, engine and lint, no new type list; the refusal placed where REST 读路径:searchFields / groupBy / aggregations 指向不存在的字段时被静默降级(#4226 收口后剩下的三条轴) #4254 put the loudness (the ingress) with the engine withhold untouched as the fail-closed floor; the branch fires only on the declared path by construction (the auto default never keeps a credential), and the comment says exactly that. The message names the type, says why the narrowing cannot be honoured, and names the declaration remedy (the declaredSet.has(first) guard is always true in this branch — harmless). The gate's own docblock in protocol.ts ("cannot admit a field the engine would then decline to scan") is now true on the credential axis where it was false. Must-fix: none. Should-fix (non-blocking): filter credential names out of the set the declared branch lists, with the same predicate, so the remedy text does not advertise a column the engine never scans.
  • packages/rest/src/data-search-fields-credential-door.test.ts — right layer (the door over a real driver), both types, both verbs, both slots, controls, zero-read pin, and a message assertion that distinguishes the new arm from the auto-default refusal. Should-fix (non-blocking): pin that a control increments reads.n once, so the zero-read assertion is proven live rather than vacuously true; the sibling harness shares the gap.
  • content/docs/api/data-api.mdx — the row fits the table's grammar, states the refusal and the remedy, and is accurate against the gate.
  • .changeset/27829-search-fields-credential-refused.md — before / now / unchanged / who-sees-it / remedy, both declaration lines, exactly one ADR-0087 marker, BREAKING banner; the "who sees it" paragraph matches what the Console producers actually send.

Implemented-by: claude/issue-27829-searchfields-credential-refused
Reviewed-by: session_01L6bhgUCizoYWQVJWjhdR6F

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

维护者速读(席位终稿)

  • 改了什么:对象的 searchableFields 声明了 secret / password 字段时,请求里的 $searchFields(以及 POST 的 searchFields、对象形 search.fields)只要点名该字段,数据接口就回 400 INVALID_FIELD 并点出字段名,不读库。
  • 为什么改:以前门放行这个名字,引擎再按凭据规则把该列拿掉,结果悄悄改搜声明里的其余字段。调用方想搜 api_key,拿回的是按 name 匹配的行,看不出任何异常。
  • 风险与回滚:只影响在 $searchFields 里点名凭据字段的调用方。Console 列表搜索原样回传视图的 searchableFields,所以视图若声明了凭据字段,搜索会从 200 变成 400;这种声明 lint 早已报为错误。引擎侧的凭据隐藏不动,仍是兜底。回滚 = revert 本 PR。
  • 先看:packages/metadata-protocol/src/protocol-query-validation.ts 中 assertSearchFieldsAreSearchable 新增的 credential 一支。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/data-search-fields-credential-door.test.ts,应 6/6 通过。
  • 需要你做的:无。席位落地。

席位意见: ACCEPT。收窄与引擎的凭据谓词完全一致,每种写法都经过同一道门,契约复核 PASS。

席位核对

ACCEPT · head f6a224d16daa1c9f5448edbee491053a73c7d1ec · 2026-10-30T05:06Z

  • 形态: 4 files (+198/−1): gate branch in protocol-query-validation.ts, door pin, docs row, changeset. No packages/spec, no line of protocol.ts. Tier S.
  • Clause-②: no (narrowing); ! in title, BREAKING banner, ADR-0087 not-required (no-migration-prescription); Metadata-protocol: none; Skills-docs: none.
  • 前提实测: the card's reading reproduced red at base 637cfc67 ($searchFields=api_key → 200 with the name matches), dev report 6515056445.
  • 抽查读数: contract review 6515260948 PASS on this head: predicate parity exact (isCredentialFieldType = the engine's collectCredentialFields and the lint's check; FieldType has no other credential member); every override spelling and the export route reach the gate through findData; Console producers send the view's list only. Narrowing dogfood battery at this head: 357 files passed, 2875 tests passed, 0 failed.
  • 门禁 check-runs: 54 on this head: 40 success, 14 skipped, 0 other (read 2026-10-30T05:06Z). Derived gates 90/90 exit 0, 0 NOT-MEASURED.
  • 接受的偏差: the review's two non-blocking should-fixes: (1) the declared "not searchable" message still lists a declared credential in allowed (cosmetic); (2) the zero-read counter has no positive control (live in practice, since the engine reaches driver.find; the sibling door test shares this).
  • 范围外去向: none filed; the two should-fixes and the review's ③ observations (searchAll skip judgement, lint explanation grain) are cosmetic and recorded here.
  • 散文面: changeset, the docs row and the 400 message, read by the seat and the review.
  • 落地条款: relay pr_ready + automerge_enable through the merge queue.

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