Skip to content

fix(service-datasource)!: refuse a top-level password or connectionString at the datasource admin write doors - #28607

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-9kvath
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-9kvath

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #28549

Clause-②: no (narrowing) — a create or edit body that carries password or connectionString at its top level, which POST /api/v1/datasources answered 201 (and PATCH /api/v1/datasources/NAME 200) with the key silently dropped, is refused 400 DATASOURCE_ADMIN_ERROR before anything is written. Nothing is widened.
Metadata-protocol: none
Skills-docs: none — no page under skills/** or content/docs teaches the admin write body; the refusal text is DatasourceSchema's own, which already ships with the spec and does not move.

BREAKING accept-set narrowing at the datasource admin write doors (@objectstack/service-datasource).

维护者速读

  • 改了什么:数据源管理接口的创建(POST /api/v1/datasources)和编辑(PATCH /api/v1/datasources/NAME)现在会拒收写在请求体顶层的 password 或 connectionString:400 DATASOURCE_ADMIN_ERROR,点名这个键,不回显值,什么都不写入。拒收文字直接取自 DatasourceSchema 自己的报错,也就是 os compile 打印的那一段(password → 用秘密字段或 external.credentialsRef 绑定;connectionString → 放进 config.url)。
  • 为什么改:卡 Datasources: POST /api/v1/datasources accepts and silently drops a top-level password or connectionString that os compile refuses, answering 201 #28549(分诊 6554155790):以前这两个键被悄悄丢掉、接口照样答 201,作者拿到一个连不上的数据源且没有任何解释,而构建门对同一个数据源会点名拒收。
  • 风险与回滚:只收窄这两个顶层键;用 secret 字段传凭据、编辑时保留已绑定的 external.credentialsRef、config.password 的原有拒收都不变。控制台表单只发 label/driver/schemaMode/config/external/secret,不会触发。回滚即 revert。
  • 先看:packages/services/service-datasource/src/datasource-admin-service.ts(新加的 assertNoTopLevelCredential 及其在 create / update 里的两处调用);packages/services/service-datasource/src/__tests__/datasource-admin-top-level-credential.test.ts;.changeset/28549-datasource-top-level-credential.md。
  • 两分钟验证:pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/datasource-admin-top-level-credential.test.ts → 10/10 通过。
  • 需要你做的:无,席位落地(收窄需先过合约评审)。

What changes

The doors (packages/services/service-datasource/src/datasource-admin-service.ts). A new private assertNoTopLevelCredential(name, body) runs at the start of both write doors, before anything is read from or written to the store:

  • createDatasource: right after the name rule and the draft external.credentialsRef refusal.
  • updateDatasource: right after the 404 and the code-defined refusal, before the merge. It runs on every patch, a label/active-only edit included: that exemption spares a row stored before a gate, while this judges a credential the author is writing now.

It looks for the keys in TOP_LEVEL_CREDENTIAL_KEYS (password, connectionString) on the body. When one is present it asks DatasourceSchema for its refusal of those key names, with null standing in for every value, and keeps the root unrecognized_keys message. It then throws Invalid datasource 'NAME'. plus that message, the heading the existing record judgement (assertValidRecord) uses. The route turns the throw into 400 DATASOURCE_ADMIN_ERROR, as it does for every refusal from this service.

One spelling. The prescription is not copied anywhere. The message is the strict shape's own refusal from packages/spec/src/data/datasource.zod.ts (the password and connectionString guidance rows), the same text os compile prints. The pin asserts the door's message equals Invalid datasource 'NAME'. plus DatasourceSchema's message derived inside the test, so a re-inlined copy that differs by a word goes red.

No credential stored or echoed. Only key names reach the judgement; the values never do. The refusal is thrown before writeSecret, putDatasourceRecord or any pool seam runs. The tests read the store, the secret store and the op log afterwards, and check that the answer holds neither the placeholder password nor the DSN host.

Tests

New file src/__tests__/datasource-admin-top-level-credential.test.ts. It drives the real routes over HonoHttpServer with the real DatasourceAdminService on an in-memory record store and secret store, using the entitled-caller.fixture caller. Every credential is an obvious placeholder (PLACEHOLDER_NOT_A_SECRET, db.example.invalid).

  • Refusals (each asserts status 400 + error.code DATASOURCE_ADMIN_ERROR + success: false, the exact message, the value absent, and nothing stored, minted or registered):
    • POST with a top-level password (the card's repro 1); the message also names external.credentialsRef.
    • POST with a top-level connectionString carrying URL userinfo (repro 2).
    • POST with both keys: one answer naming both.
    • POST with a top-level password beside a secret.
    • PATCH with a top-level password, and with a top-level connectionString: the stored record is unchanged.
    • PATCH that is label-only plus a password.
  • Controls:
    • POST with the credential as secret: 201, a sys_secret: ref bound at external.credentialsRef, the placeholder held only in the secret store.
    • PATCH on a record with a bound external.credentialsRef: 200, ref kept.
    • POST with config.password: still refused with the driver contract's Invalid configuration for driver 'postgres'. naming config.password, unchanged.
Red first, green, ablation
  • Red first (test written and run against the unchanged base service): Tests 7 failed | 3 passed (10). The four POST refusals were expected { status: 201, success: true, … } to deeply equal { status: 400, success: false, … }, and the three PATCH refusals answered status: 200. The three controls were green.
  • Green at the fix: Tests 10 passed (10).
  • Ablation (node scripts/ablation-replace.mjs, anchor if (present.length === 0) return; → if (present.length >= 0) return;, which turns the check off at both call sites): anchor 1 → 0, replacement 0 → 1, blob fae370d6fc02 → 7e11b57f76be. Result: the 7 refusal cases red again (POST back to 201, PATCH back to 200) and the 3 controls green. Restore: blob equals HEAD (fae370d6fc02), git diff HEAD empty. The test imports the service from src/, so no build sits between the mutation and the run.
  • pnpm --filter @objectstack/service-datasource test: Test Files 67 passed (67), Tests 1110 passed (1110). pnpm --filter @objectstack/service-datasource typecheck exit 0, and tsc --listFiles includes the new test file.

Acceptance notes

  • Which keys. The build door's strict shape refuses every undeclared top-level key. Its credential prescription ("must never be inlined … bind through external.credentialsRef") is given for password. connectionString gets "belongs inside config … config: { url: … }", and a config.url with userinfo is then refused by the driver contract, which points at the secret. The doors take each key's text verbatim from the schema, so connectionString answers with the config.url prescription, not the credentialsRef one. That is "the same prescription the build door gives" for that key.
  • Other top-level keys these doors still drop silently (read from toRecord and the update merge, which project only name, label, driver, schemaMode, config, external, pool, active): the other connection keys the schema has guidance for (url, host, port, database, user, username, filename), connector, the retired blocks (readReplicas, capabilities, retryPolicy, healthCheck, …), and any other unknown key. A top-level url can carry userinfo exactly as connectionString does. Out of scope by the order ("a general unknown-key refusal for every top-level key is out of scope"). A closed body would also have to tolerate what a client round-trips from GET /datasources/NAME (origin, hasSecret, redactedConfigKeys, definedIn).
  • The unsaved-draft probe (POST /api/v1/datasources/test, testConnection) projects through the same toRecord and still ignores a top-level password: the probe then fails to connect with the driver's own error, not a silent green. It is not a write door, so it is left as it was.
  • Control premise. The order's control "a valid body with external.credentialsRef still answers 201" does not hold on main: since the own-reference rule, create refuses a draft that names external.credentialsRef. The controls are a create whose credential arrives as secret and is bound at external.credentialsRef (201), and an edit that keeps a bound external.credentialsRef (200).
  • Producers measured. The Console's datasource editor (ui/packages/app-shell/src/views/metadata-admin/datasource/DatasourceResourcePage.tsx, draftBody) sends label, driver, schemaMode, config, external, secret and never a top-level credential. packages/client exposes only the datasources/NAME/external/* family.

Verification

All readings on head 0cc981aa (the only commit; nothing landed after the union ran).

  • Derived gate families: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 61 families, each run with its exit code captured before any pipe (stdin from /dev/null). --ran reconciliation: ✓ dispatch-gates --ran: 61 derived famil(ies) accounted for — 61 run, 0 NOT-MEASURED. Two first reads were container prerequisites, re-read after meeting them:
    • check-plugin-teardown-shape.mjs --self-test exit 3 → git fetch --depth=1 origin 621a4876… → exit 0 (48 cases pass).
    • check:dual-build-cjs-loads exit 3 → full pnpm build --concurrency=2 (117/117 tasks) → exit 0.
  • Hand gates: node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD exit 0; node scripts/check-platform-checklist.mjs --changed-from origin/main exit 0 (no anchored file in the change set); node scripts/check-nul-bytes.mjs exit 0; pnpm exec eslint --no-inline-config packages/services/service-datasource exit 0, no findings. The repo-wide pnpm lint is CI's.
  • Full dogfood (owed by the ! and the (narrowing) arm): after the @objectstack/dogfood^... build, under the lock, pnpm --filter @objectstack/dogfood test → Test Files 364 passed | 3 skipped (367), Tests 2934 passed | 28 skipped (2962); no failure.
  • CI families (Test Core incl. test:repo, the lint farm, the type-check lanes) are read off this PR's check-runs by the seat.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5moUefysT3ba6SpdYY

…ring at the datasource admin write doors

POST /api/v1/datasources and PATCH /api/v1/datasources/:name projected the
body onto the record's known keys, so a top-level password or
connectionString vanished and the write answered success while os compile
refuses the same datasource by name. Both doors now refuse either key before
anything is written, with DatasourceSchema's own refusal for that key under
the heading the record judgement uses. Only key names are judged, so the
answer never echoes the credential.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5moUefysT3ba6SpdYY
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-datasource, touching 7 documentable anchor(s).

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

  • content/docs/data-modeling/drivers.mdx (via connectionString (literal, a string literal in TOP_LEVEL_CREDENTIAL_KEYS))
  • content/docs/deployment/cli.mdx (via unrecognized_keys (literal, a string literal in assertNoTopLevelCredential))
  • content/docs/protocol/objectql/types.mdx (via unrecognized_keys (literal, a string literal in assertNoTopLevelCredential))

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

  • content/docs/releases/v17/17-0.mdx (via unrecognized_keys (literal, a string literal in assertNoTopLevelCredential))

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 60 of 213 client-bound route-ledger rows — the other 153 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 153: 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; 98 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 — 3 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 ce001f97abbe460d9a6cc9a4bde6c6e38c9d37f8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c67601d083dcc6d1a217446eaee2da25b2a50b4a — the merge of head 0cc981aa238a292eff24079a3dfcb7a9fe55df1a into base ce001f97abbe460d9a6cc9a4bde6c6e38c9d37f8, 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 c67601d083dcc6d1a217446eaee2da25b2a50b4a && git checkout c67601d083dcc6d1a217446eaee2da25b2a50b4a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ce001f97abbe460d9a6cc9a4bde6c6e38c9d37f8 0cc981aa238a292eff24079a3dfcb7a9fe55df1a && git checkout -B drift-repro ce001f97abbe460d9a6cc9a4bde6c6e38c9d37f8 && git merge --no-ff 0cc981aa238a292eff24079a3dfcb7a9fe55df1a

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

⚠️ 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 ce001f97abbe460d9a6cc9a4bde6c6e38c9d37f8 → 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#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0cc981aa238a292eff24079a3dfcb7a9fe55df1a
Local-runs: none
Scope: packages/services/service-datasource/src/datasource-admin-service.ts, packages/services/service-datasource/src/tests/datasource-admin-top-level-credential.test.ts + packages/spec/src/data/datasource.zod.ts, packages/spec/src/shared/strict-object.ts, packages/spec/src/shared/suggestions.zod.ts, packages/services/service-datasource/src/admin-routes.ts, packages/spec/src/api/error-code-ledger.zod.ts, packages/services/service-datasource/src/contracts/datasource-admin-service.ts, ui/packages/app-shell/src/views/metadata-admin/datasource/DatasourceResourcePage.tsx, packages/client/src/index.ts, docs/qa/platform-checklist/areas/integration-system.json

Inputs read: card #28549 (body, triage 6554155790, claim 6554684433, dev report 6555151884), PR #28607 body, file list and net diff at the head above, the 45 check-runs on that head, and the cited code at origin/main (ce001f97abb). The narrowing face has no path hits; the floor is every non-prose path of the diff, listed first on the Scope: line. Nothing was checked out, built or run.

① Derived judgments

  1. Accept-set narrowing at the two write doors — right. POST /api/v1/datasources and PATCH /api/v1/datasources/NAME refuse a body carrying password or connectionString at its top level. Read on main: createDatasource spreads this.toRecord(input) (known keys only: name, label, driver, schemaMode, config, external, pool, active) and updateDatasource merges the same known keys, so both keys vanished before assertValidRecord ran. The premise holds at origin/main, and the diff's assertNoTopLevelCredential runs before any store read or write at both doors.
  2. The refusal code is already ledgered at this door — right, no published-surface addition. DATASOURCE_ADMIN_ERROR is registered under @objectstack/service-datasource in packages/spec/src/api/error-code-ledger.zod.ts (the row at line 1374, "lifecycle/validation refusal from the datasource-admin service"), and the route's badRequest relays every plain throw from the admin service as 400 with SERVICE_ERROR_CODE['datasource-admin']. The diff adds no code, no status and no envelope field. The docs ledger pages (content/docs/references/api/error-code-ledger.mdx, contract.mdx) already list it.
  3. One spelling of the prescription — right. The door calls DatasourceSchema.safeParse on an object holding only the present key names with null values and keeps the root unrecognized_keys message. That message is composed by strictUnknownKeyError from the schema's own guidance rows (password and connectionString: belongsInConfig('connectionString', 'url') in packages/spec/src/data/datasource.zod.ts), the same path os compile prints through. No prescription text is copied into the service; the test's specRefusal derives the expected text from the spec at run time and throws if the spec stops refusing the key, so a re-inlined copy or a spec drift reds the pin. The heading Invalid datasource 'NAME'. is the one assertValidRecord already uses.
  4. Both doors closed consistently — right. Create: after the name rule and the draft credentialsRef refusal, before the driver requirement. Update: after the 404 and the code-defined refusal, before the merge, on every patch including a label/active-only one. That is the correct reading of the [finding] POST /api/v1/datasources (and the PATCH door) never run DatasourceSchema's cross-field refinements — a mongo credentialsRef pairing that os build and PUT /meta refuse is accepted 201, and the bound secret is silently unused #21058 exemption: RECORD_JUDGED_PATCH_KEYS spares a row stored before a gate; a credential in the body being written now is not that row.
  5. No value reaches the judgement or the answer — right. Only key names are parsed (values replaced by null); the message cannot carry the credential. The tests assert the placeholder password and the DSN host are absent from the answer and that the record store, secret store and op log are untouched.
  6. No legitimate producer sends either key today — right. Console editor draftBody sends label, driver, schemaMode, config, external, secret only, and its test-connection call sends driver, config, secret. packages/client exposes only the datasources/NAME/external/* family (no create or update method). The CLI's datasource commands (introspect, list-tables, validate) are read-only. No seed or fixture under examples/, apps/ or packages/qa posts a top-level password or connectionString; the QA refusal-matrix item prescribes the credential "in the top-level secret, never in config", which the route splits out before the service sees the draft and which stays accepted (control: 201, sys_secret: ref bound).
  7. DatasourceDraft and every export unchanged — right. The contract never declared password or connectionString; the new constant and method are module-private.
  8. Metadata-protocol: none — right. DatasourceSchema, the surface a metadata author writes against, does not move; the admin REST body is not that surface, and what it now refuses the schema already refused.

② Semver level

.changeset/28549-datasource-top-level-credential.md: '@objectstack/service-datasource': patch, a BREAKING banner, Clause-②: no (narrowing) and Metadata-protocol: none as line-initial declarations, and exactly one ADR-0087 marker (not-required (no-migration-prescription)) with its reason. This matches the diff: a non-protocol breaking change on a released package ships patch and keeps the banner and disposition; nothing at rest needs a migration because a refused body was never stored. The PR body carries the same Clause-②: no (narrowing) line at the start of a line. Check Changeset and Governed Surface Queue Guard both concluded success on this head. The Clause-②: line reads no (narrowing) — the one spelling this face is keyed on; it is exact on the PR body and the changeset alike.

③ Boundary flags

open_questions: []. The report's three deviations, each answered:

  • Control premise changed (the order's "valid body with external.credentialsRef still answers 201" does not hold on main): confirmed in the cited code — assertNoDraftCredentialsRef (secrets: confirm the other stored-reference families (settings-service handles, datasource external.credentialsRef) apply the #27582 own-reference rule at their write doors #27722) refuses a draft naming external.credentialsRef on create. The substituted controls (create with secret bound at external.credentialsRef, edit keeping a bound ref) are the right ones. Answered.
  • connectionString answers with the config.url prescription, not the credentialsRef one: that is the spec's own guidance row for that key, so it is "the same prescription the build door gives" for that key. A DSN carrying userinfo then meets the driver contract's refusal at config.url — a two-step the spec text already implies and the changeset's remedy line states up front ("without userinfo"). The spec is read here, not reviewed; no change asked of this PR. Answered.
  • test:repo not run locally: CI's Test Core shards own it; they are in progress on this head at the time of this record (see the gate reading below). Answered as an honest reading, not a local run.

Check-runs on 0cc981aa at the time of writing: 45 runs, 0 failures; 30 completed (success or skipped — among them Check Changeset, Governed Surface Queue Guard, PR-body guards, Check PR Size, Lint · ESLint, Lint · scoped · query-options erasure, Lint · scoped · tooling self-tests, Lint · PM dispatch-gates, Type Check · debt ledger, Type Check · source gates, Dogfood Regression Gate, Test UI, Live E2E UI); 15 still in_progress (Test Core 1–8, Build Core, Type Check · workspace, Type Check · consumer gates, and four scoped lint lanes). Those conclusions are the gate verdicts for their families; this record does not pre-empt them, and the queue stays gated on them. The dev's local readings (61 derived families run, service suite 1110/1110, dogfood 2934 passed, ablation red/green) are the PR's account, read but not relied on.

④ Design judgment

The narrowing, judged against the published spec on main: DatasourceSchema is a strictObject whose guidance names password ("must never be inlined … bind through external.credentialsRef") and connectionString (belongs in config as url). The build door and the meta door already refuse these keys by name; the admin door alone accepted the body and dropped them. Nothing declared is taken away at the consumer: DatasourceDraft never carried either key, no stored record ever held one, and the only observable before was a success answer over a datasource that could not connect. This makes the spec's own refusal real at the last door that did not give it. Right.

Attempts to break it, each read in the diff and the cited code:

  • { password: null } is refused (presence is !== undefined), as the build door refuses the key whatever its value. Consistent.
  • A body with secret beside password: the route's splitSecret removes secret first, so the draft the service judges still carries password and is refused; the secret channel itself is unchanged (control green). Consistent.
  • A code-defined datasource patched with a password: the code-defined refusal fires first. Acceptable ordering — the row is read-only before the body is judged.
  • Spec drift: if DatasourceSchema ever declared one of these keys, detail.length === 0 returns and the door falls open silently — but the test's specRefusal throws "fixture premise broken" in exactly that case, so the pin goes red rather than the door going quiet. Sound.
  • Direct service callers with a malformed input: assertValidName(input?.name) throws before the new check reads input; updateDatasource already dereferenced patch.label, so an undefined patch was never accepted. No regression.
  • The route answers every thrown Error from this service as 400 DATASOURCE_ADMIN_ERROR unless it carries the 503 or 404 envelope; the new throw carries neither. Right code, right status.

Findings:

  • should-fix (follow-up card, not this PR): POST /api/v1/datasources/test (testConnection) still projects a draft through toRecord before judging it, so a top-level password or connectionString on the probe is dropped silently and the probe tests a different connection than the one the author described — green on a server that needs no password, a driver-side failure otherwise, with no mention of the key. It writes nothing and the card scoped the write doors, so it is not a must-fix here, but the same spec refusal is owed at that door for the same reason.
  • should-fix (give it a carrier): the dev's out-of-scope finding that every other undeclared top-level key (url with userinfo among them) is still dropped silently at these doors has carrier: none. A general closed body at the admin door is a change of its own (it must tolerate the GET round-trip keys origin, hasSecret, redactedConfigKeys, definedIn); it should be filed so it is not lost in a PR's acceptance notes.
  • No must-fix.

Implemented-by: claude/project-thread-9kvath
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

席位核对 · ACCEPT · PR #28607 · 2026-11-01T08:06Z

Head-sha: 0cc981aa238a292eff24079a3dfcb7a9fe55df1a

维护者速读(席位终稿)

  • 改了什么:数据源接口的创建和编辑,现在拒收写在请求体顶层的 password 或 connectionString:400 DATASOURCE_ADMIN_ERROR,点名这个键、不回显值、什么都不存。拒收文字直接取自 DatasourceSchema,和 os compile 打印的一样。
  • 为什么改:以前这两个键被悄悄丢掉、仍答 201,作者拿到连不上的数据源却没有任何说明(Datasources: POST /api/v1/datasources accepts and silently drops a top-level password or connectionString that os compile refuses, answering 201 #28549)。
  • 风险与回滚:只收窄这两个顶层键;控制台表单从不发它们(已核)。回滚即 revert。
  • 先看:datasource-admin-service.ts 的 assertNoTopLevelCredential 与 create / update 两处调用。
  • 两分钟验证:pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/datasource-admin-top-level-credential.test.ts → 10/10。
  • 需要你做的:无。席位按常设授权落地。
  • 席位意见:契约复核 PASS;测试从 spec 现取期望文字,spec 漂移即红。可落。

核对六项

  1. 形态:draft → main;Fixes #28549,本 PR 交付卡上两扇写门,合并即关卡正确;其余闭合关键词零命中(已扫)。文件 3 个,在认领申报面内。
  2. 前提实测:红先在 base 上 7/10 红(两键两门均 201/200 且不存);消融逐门恢复 201/200。
  3. 门禁:本 head check-runs 48 个,35 success、13 skipped、0 失败;Lint & Repo Gates、TypeScript Type Check 皆 success。dev 本地 61 族全测、0 NOT-MEASURED;收窄 PR 的 dogfood 电池钉本 head 0cc981aa:364 文件过、3 跳过、无失败。
  4. 接受的偏差:派发令的对照「带 external.credentialsRef 的草稿创建答 201」在 main 上不成立(创建拒收草稿自带的 credentialsRef),dev 换成「secret 绑定创建 201」与「保留已绑 ref 的编辑 200」,接受;connectionString 答的是 schema 为该键给的 config.url 处方,即构建门对该键的同一处方,接受。
  5. 范围外去向:测试门 POST /datasources/test 仍丢顶层键、其余未声明顶层键仍被两门静默丢弃 → filed Datasources: the test door and every undeclared top-level key still bypass the refusal DatasourceSchema gives (the rest of #28549) #28612(Datasources: POST /api/v1/datasources accepts and silently drops a top-level password or connectionString that os compile refuses, answering 201 #28549 的子卡,已派发,基于本 PR 叠加);GET 回显字段的往返容忍写入 Datasources: the test door and every undeclared top-level key still bypass the refusal DatasourceSchema gives (the rest of #28549) #28612 的读物(本 PR Acceptance notes)。Metadata-protocol: none,无 Skills-docs: 欠账。
  6. 落地条款:Clause-②: no (narrowing) ⇒ 契约复核达档 PASS 在案:comment 6555221422(CONTRACT_REVIEW_TIER,同 head 0cc981aa)。check-governed-merges --pr 28607:NOT governed,340 行。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

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

Development

Successfully merging this pull request may close these issues.

Datasources: POST /api/v1/datasources accepts and silently drops a top-level password or connectionString that os compile refuses, answering 201

1 participant