Repository navigation
fix(service-datasource)!: refuse every top-level key DatasourceSchema refuses at the datasource test, create and update doors - #28632
Conversation
…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
… refuses at the datasource test, create and update doors The unsaved-draft probe (POST /api/v1/datasources/test) and the two write doors built their record from the known keys only, so a top-level password on the probe, and any other key the strict shape refuses (url one level too high, an alias such as type, a retired block, an unknown key) on all three doors, vanished without a word. The doors now refuse them 400 DATASOURCE_ADMIN_ERROR with DatasourceSchema's own root unrecognized-key refusal, derived from the body's key names at run time; values are never judged or echoed. Co-Authored-By: Claude <noreply@anthropic.com>
…el key refusal Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com> # Conflicts: # packages/services/service-datasource/src/__tests__/datasource-admin-top-level-credential.test.ts # packages/services/service-datasource/src/datasource-admin-service.ts
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 3 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 cb8536666eeb683ee9b444cfa0bfae7d645bbcb8 && git checkout cb8536666eeb683ee9b444cfa0bfae7d645bbcb8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d77dc07a5e5a4b5478763fb3a2126913c92f8c01 f1438f52c4d31ddd71759cafd70dda5e3f9bc3f3 && git checkout -B drift-repro d77dc07a5e5a4b5478763fb3a2126913c92f8c01 && git merge --no-ff f1438f52c4d31ddd71759cafd70dda5e3f9bc3f3
node scripts/docs-audit/affected-docs.mjs --json d77dc07a5e5a4b5478763fb3a2126913c92f8c01
|
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH Contract reviewServed-tier: Inputs read: card #28612 (body, claim 6555258668, dev report 6556117106), parent #28549 (body, triage, claim, dev report 6555151884, landing record) and the parent's contract review 6555221422 on PR #28607, PR #28632 body, file list and net diff at the head above, the 45 check-runs on that head, and the cited code at ① Derived judgments
② Semver level
③ Boundary flags
Check-runs on ④ Design judgmentThe narrowing, judged against the published spec on Attempts to break it, each read in the diff and the cited code:
Findings:
Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH 席位核对 · ACCEPT · PR #28632 · 2026-11-01T09:47ZHead-sha: 维护者速读(席位终稿)
核对六项
|
Requested by Leehom · project thread
Fixes #28612
Clause-②: no (narrowing) — three doors refuse top-level keys they silently dropped:
POST /api/v1/datasources/test(a top-levelpassword/connectionString, and any other keyDatasourceSchemarefuses),POST /api/v1/datasourcesandPATCH /api/v1/datasources/NAME(every key the schema refuses, beyond the two credential keys PR #28607 closed). Each now answers400 DATASOURCE_ADMIN_ERRORbefore anything is read, written or probed. Nothing is widened.Metadata-protocol: none
Skills-docs: none — no page under
skills/**orcontent/docsteaches the admin request body; the refusal text isDatasourceSchema's own, which already ships with the spec and does not move.BREAKING accept-set narrowing at the datasource admin doors (
@objectstack/service-datasource).Built on PR #28607 (same file, landed as
f3d84b33): this branch started from its head and has mergedorigin/mainsince, so the diff is only this change.维护者速读
POST /api/v1/datasources/test、创建POST /api/v1/datasources、编辑PATCH /api/v1/datasources/NAME)现在会拒收请求体顶层任何DatasourceSchema不认识的键:400DATASOURCE_ADMIN_ERROR,点名这个键,不回显值,什么都不写入、也不发起探测。哪些键算"不认识"、提示怎么说,都在运行时直接取自DatasourceSchema自己的报错(即os compile打印的那段),服务里没有手写清单。password,测的其实是另一个连接;三个入口都会悄悄丢掉url、host、type、readReplicas、拼错的键等,接口照样答成功。origin、description、ssl、autoConnect等)照常通过;secret由路由先拆走,仍是唯一的凭据通道。控制台、packages/client、CLI、QA 清单与 dogfood 都不发被拒的键。唯一可能受影响的是把GET的应答(含hasSecret、redactedConfigKeys等)原样 PATCH 回来的客户端,仓内实测没有这样的生产者。回滚即 revert。packages/services/service-datasource/src/datasource-admin-service.ts(assertNoUndeclaredTopLevelKey与testConnection开头的新调用);packages/services/service-datasource/src/__tests__/datasource-admin-top-level-credential.test.ts(Datasources: the test door and every undeclared top-level key still bypass the refusal DatasourceSchema gives (the rest of #28549) #28612 的三个 describe);.changeset/28612-datasource-undeclared-top-level-key.md。pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/datasource-admin-top-level-credential.test.ts→ 25/25 通过。What changes
One check, three doors (
packages/services/service-datasource/src/datasource-admin-service.ts). PR #28607's privateassertNoTopLevelCredential(name, body)and its hand listTOP_LEVEL_CREDENTIAL_KEYSbecomeassertNoUndeclaredTopLevelKey(heading, body), with no list:undefined), gives each the valuenull, parses that skeleton throughDatasourceSchema, and keeps only the strict shape's rootunrecognized_keysrefusal. So the key set is exactly what the schema refuses at run time, and the message is the schema's own (itsguidancerows forpassword,connectionString,url,host,connector, the retired blocks, its alias table fortypeand the like, and its generic unknown-key text for anything else). Values never reach the parse, so the answer cannot echo a credential.testConnection(the unsaved-draft probe): first statement, before the driver requirement, the config judgement, the record judgement and the probe. It throws, so the route answers400 DATASOURCE_ADMIN_ERROR(the card asks for that code), under the heading this door already gives a draft it refuses:This connection draft is not a valid datasource.createDatasourceandupdateDatasource: the same position as fix(service-datasource)!: refuse a top-level password or connectionString at the datasource admin write doors #28607's call (create: after the name rule and the draftcredentialsRefrefusal; update: after the 404 and the code-defined refusal, before the merge, on every patch including alabel/active-only one), under the headingInvalid datasource 'NAME'.Door-only fields. The service's
DatasourceDraftdeclaresname,label,driver,schemaMode,config,external,pool,active: all declared by the schema. The one field the doors accept on purpose that the schema does not carry issecret, the credential channel: the route'ssplitSecretremoves it before the service sees the draft, so it never reaches the check (controls below, on all three doors). No other door-only field exists.Keys the schema declares pass.
origin,description,ssl,autoConnect,nameon an edit, and the metadata protection fields are declared, so the key check lets them through even where the projection leaves them out (originis still stampedruntimeby the door). That is the card's scope: refuse what the schema refuses.Tests
Three new
describeblocks insrc/__tests__/datasource-admin-top-level-credential.test.ts, same harness as #28607 (real routes overHonoHttpServer, realDatasourceAdminServiceon in-memory stores,entitled-caller.fixture). The probe seam now recordsprobein the op log, so "nothing probed" is read, not assumed.error.codeDATASOURCE_ADMIN_ERROR+success: false, exact message = heading +DatasourceSchema's refusal derived in the test, value absent, op log empty so no probe ran): top-levelpassword,connectionString(URL userinfo),foo, andpasswordbeside asecret.foo,url(URL userinfo),type(the schema's alias row),readReplicas(a retired block), andfoo+host+passwordin one answer.foowith aconfig,fooon alabel-only edit,hasSecret(aGETanswer key) on alabel-only edit.secret(200,ok: true, one probe); create withdescriptionandorigin(declared) answers 201 and storesorigin: 'runtime'; an edit carryingnameandoriginanswers 200. fix(service-datasource)!: refuse a top-level password or connectionString at the datasource admin write doors #28607's controls (secret-bound create 201, edit keeping its bound ref 200,config.passwordrefusal) stay green.Red first, green, ablation
0cc981aa, service unchanged):Tests 12 failed | 13 passed (25). The test door answeredexpected { status: 200, success: true, … } to deeply equal { status: 400, success: false, … }(the probe ran without the key), create answeredstatus: 201, edit answeredstatus: 200. All controls, fix(service-datasource)!: refuse a top-level password or connectionString at the datasource admin write doors #28607's included, were green.Tests 25 passed (25).node scripts/ablation-replace.mjs, each call toassertNoUndeclaredTopLevelKeyreplaced byvoid input;/void patch;; the test imports the service fromsrc/, so no build sits between the mutation and the run):30943306cc1c→ef800604f535;Tests 4 failed | 21 passed, exactly the four test-door refusals.30943306cc1c→32f0578239a4;Tests 9 failed | 16 passed, the four Datasources: POST /api/v1/datasources accepts and silently drops a top-level password or connectionString that os compile refuses, answering 201 #28549 POST refusals and the five Datasources: the test door and every undeclared top-level key still bypass the refusal DatasourceSchema gives (the rest of #28549) #28612 POST refusals.30943306cc1c→3c4b56a5464b;Tests 6 failed | 19 passed, the three Datasources: POST /api/v1/datasources accepts and silently drops a top-level password or connectionString that os compile refuses, answering 201 #28549 PATCH refusals and the three Datasources: the test door and every undeclared top-level key still bypass the refusal DatasourceSchema gives (the rest of #28549) #28612 PATCH refusals.30943306cc1c),git diff HEADempty.Acceptance notes
GETround-trip keys are refused, not tolerated. fix(service-datasource)!: refuse a top-level password or connectionString at the datasource admin write doors #28607's contract review noted that a general closed body "must tolerate the GET round-trip keysorigin,hasSecret,redactedConfigKeys,definedIn". Measured:originis declared by the schema and passes. The other three (and the list row'sstatus/statusReason) are not declared, so the card's item 2 ("every top-level keyDatasourceSchemarefuses") refuses them, and no producer in the repo sends them back: the Console editor'sdraftBodybuilds a fresh body (label,driver,schemaMode,config,external,secret) and its test call sendsdriver,config,secret;packages/clienthas no create/update/test method; the CLI datasource commands are read-only; the QA checklist bodies and the dogfood suites send declared keys only. Tolerating them would be a consumer-side allowance for keys nothing writes. The changeset's remedy line says not to send them back.description,ssl,autoConnect(and the protection fields) pass the key check and are then left out bytoRecord/ the update merge, as before. They are accepted by the schema, so they are outside this card ("refuse what the schema refuses"); reported to the seat as an out-of-scope finding.ui/packages/app-shell/src/views/metadata-admin/datasource/,packages/client/src,packages/cli/src,packages/qa/dogfood/test,docs/qa/platform-checklist/areas,examples/,apps/,skills/,content/docs.200withresult.ok: false. This one answers400 DATASOURCE_ADMIN_ERROR, as the card's item 1 asks ("the sameDATASOURCE_ADMIN_ERROR"): a key that changes what is being tested is a malformed request, not a failed connection.Verification
All readings on head
f1438f52(the merge oforigin/mainatde3fafcb, after #28607 landed asf3d84b33; the diff againstmainis the three files above).pnpm --filter @objectstack/service-datasource test→ Test Files 67 passed (67), Tests 1125 passed (1125).pnpm --filter @objectstack/service-datasource typecheckexit 0;tsc --listFilesincludes the test file.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack→ 61 families (same list before and after the merge), each run after a fullpnpm build --concurrency=2(117/117) with its exit code captured before any pipe and stdin from/dev/null: 61 × exit 0.--ran:✓ dispatch-gates --ran: 61 derived famil(ies) accounted for — 61 run, 0 NOT-MEASURED.node scripts/check-commit-card-trailers.mjs --range origin/main..HEADexit 0;node scripts/check-platform-checklist.mjs --changed-from origin/mainexit 0 (no anchored file in the change set);pnpm exec eslint --no-inline-config packages/services/service-datasourceexit 0, no findings. The repo-widepnpm lintis CI's.!and the(narrowing)arm), after the full build, under the lock,pnpm --filter @objectstack/dogfood test→ Test Files 1 failed | 364 passed | 3 skipped (368), Tests 1 failed | 2938 passed | 28 skipped (2967). The one failure is not this change:route-ledger-live-mount-parity.dogfood.test.tsreportsruntime/route-ledger.ts: GET /packages/:id/customizations — LEDGERED BUT NOT MOUNTED, a runtime route that arrived with feat(runtime,spec): GET /packages/:id/customizations answers a base's overrides, extensions and switched-off items in one read #28595 (519089e1) in the mergedmain. This diff touches onlyservice-datasource. Reported to the seat; not fixed here.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_01FEkQ5nv4Jd9m4jHpZgRJVT