Repository navigation
feat(spec)!: a URL is judged as a URL parser cleans it — surrounding whitespace trimmed, tab and newline characters removed — with one userinfo grammar at both doors; a credential query parameter is refused in any string of a connector entry - #29875
Conversation
…a credential query parameter in any string of a connector entry WIP: implementation; tests, D3 entry and changeset follow. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…th connector doors and both datasource doors Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…nd any-string credential rules Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
… the connector and datasource save doors Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 168 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 381b7f25131f2376ea7d1ff57575f06026b8f3e7 && git checkout 381b7f25131f2376ea7d1ff57575f06026b8f3e7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9edd6e783b48ad5e326060e4c4c35040f159deac 49b7049edb9149a0bec96768e320ad9b86ed3db8 && git checkout -B drift-repro 9edd6e783b48ad5e326060e4c4c35040f159deac && git merge --no-ff 49b7049edb9149a0bec96768e320ad9b86ed3db8
node scripts/docs-audit/affected-docs.mjs --json 9edd6e783b48ad5e326060e4c4c35040f159deac
|
…ewline characters too; one userinfo grammar at both doors Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…both connector and datasource doors Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…try and changeset state it Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract-review-claim: session_01QSeKYn5CmmdShCJifm6WYj · head 49b7049 · 2026-11-03T11:30Z |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract reviewServed-tier: Inputs: the PR body as rewritten for this round, the file list, the net diff against ① Derived judgmentsAccept-set changes, per door and half, read off the tree.
Read/write symmetry ("refused ⇔ rewritten").
Accessor semantics. Check-runs on ② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj 席位核对ACCEPT · head
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no connector entry that
skills/**, the hand-writtencontent/docs/**,examples/**orpackages/apps/**author holds a credential query parameter in any string underproviderConfigorauth, and none of their datasource or connector URLs carries a credential part (scanned: 0 connector or datasource literals affected; the shipped OpenAPI documents those apps reference by path: 0), so nothing they teach changes; each refusal carries its own prescription.维护者速读
scheme://或无 scheme 的//起始);连接器条目providerConfig与auth下任意字符串里带值的凭据类查询参数一律拒绝。保存门 422INVALID_METADATA,定位到该字符串;两侧读取在写入判定的原始位置改写,其余字符原样返回。url-credential-judged-trimmed-and-in-any-connector-string)。升级后在校验、构建产物加载与保存时被拒的有:连接器providerConfig/auth的任意字符串里带值的凭据查询参数;按解析器清洗后(去首尾空白、删除制表/换行字符)含凭据部分的 URL;连接器中无 scheme 的//权限部分所带的 userinfo。仓内 examples/apps/skills/docs 实测 0 处受影响。已存行不重判;读取隐去凭据部分,原样保存保留库值。回滚即 revert 本 PR。packages/spec/src/data/driver/url-boundary.ts(唯一的边界解析:按解析器清洗并保留到原始串的偏移映射,userinfo 区间供两扇门共用)、packages/spec/src/integration/connector-url-credentials.ts(connectorUrlWithoutUserinfo同时供写判与读改写;查询参数判定作用于任意字符串)、packages/spec/src/data/datasource-credential-redaction.ts(redactUrlPassword按写入侧同一区间改写)。pnpm --filter @objectstack/spec exec vitest run src/integration/connector-entry-url-credentials.test.ts src/data/datasource-credential-redaction.test.ts,预期全部通过。What changes
One value-level rule at both doors — a URL is judged after trimming surrounding whitespace; a credential query parameter is refused in any string of the entry.
urlUserinfoSpan, the scheme-optional grammar the datasource doors already use: an authority starts atscheme://or a scheme-relative//.urlCredentialQueryParams) on every string underproviderConfigandauth, scheme or none. Same names, same located refusal, never echoing the value.packages/spec/src/data/driver/url-boundary.ts(internal, not re-exported) holds the one boundary parse:urlTrimmedBoundsandurlUserinfoSpan, offsets into the authored string. The datasource write accessors (urlUserinfoPassword,urlUserinfoUsername) read the userinfo there and the datasource read (redactUrlPassword) rewrites the password there. On the connector sideconnectorUrlWithoutUserinfois both the write door's userinfo judgement (does it change the value?) and the read's rewrite, and the query pairs go through the sharedcredentialQueryParamOf. Every other character, the surrounding ones included, is served as stored. At this head the boundary parse isurlAsParsed(the cleaned string with a map back to raw offsets) andurlUserinfoSpan(raw offsets, the cleaned userinfo, the mapped colon);connectorUrlWithoutUserinforemoves the whole userinfo and its@at those offsets.ConnectorSchemakeeps its posture; header and environment maps are judged by neither door; stored rows are never re-judged, a save of the served form keeps the stored value, and an edit stores the edit. No public export added or removed (check:api-surfaceunchanged).18.url-credential-judged-trimmed-and-in-any-connector-string.ts; changeset@objectstack/specminor, BREAKING, with its FROM → TO and the registration marker.Tests
packages/spec/src/integration/connector-entry-url-credentials.test.ts— new block: refused at the string (userinfo behind surrounding whitespace,api_key/client_secretin a URL behind whitespace,tokenand the instance'sparamNamein a string with no scheme,signaturein a string that is not a URL, anauth.tokenUrlbehind whitespace); CONTROL accepts (clean URLs with surrounding whitespace, scheme-less and non-URL strings holding none, empty credential values); the read rewrites the same position with surrounding bytes kept and the served form passes the write door; refused ⇔ rewritten overproviderConfig.baseUrl, a non-URLproviderConfigstring andauth.tokenUrl.packages/spec/src/integration/connector-credential-redaction.test.ts— the pin that held a non-URL string with a credential parameter untouched pinned the removed boundary; it is split into a still-untouched CONTROL (a non-URL string holding none) and the new rewrite pin.packages/spec/src/data/driver/driver-credential-refusal.test.ts— per URL-bearing driver key (postgres, mysql, mongo, tursourl, tursosyncUrl):user:password@refused behind each of five surroundings, and a bare username behind each not refused for a password;urlUserinfoPasswordjudges the trimmed string.packages/spec/src/data/datasource-credential-redaction.test.ts— the datasource read rewrites the password at the position the write door judges, keeps the username and every surrounding character, and the served form passes the write door; CONTROL strings come back byte for byte;redactDatasourceConfignamesurl.Dogfood, both HTTP save doors:
connector-meta-door-url-and-nested-keys.dogfood.test.tsgains a block on the existing showcase boot (five 422 refusals atPUT /api/v1/meta/connector/:namewith nothing stored; a credential-free URL with surrounding whitespace saves, materializesreadyand dispatches; a seeded row is served with the parts removed at the judged position to a member and an administrator; a save of the served form keeps the stored values). Newdatasource-meta-door-url-credential-trimmed.dogfood.test.ts(three 422 refusals atPUT /api/v1/meta/datasource/:namewith nothing stored; a bare username and a clean URL save as written; a seeded row is served with the password removed and its surrounding characters kept; the stored row untouched).Test names state the rule. Every placeholder is fake and every assertion on one is its absence from what a door served.
connector-entry-url-credentials.test.ts— a second block: refused at the string for a credential name holding a tab or newline (URL and non-URL string), a userinfo behind a scheme or//holding a tab or carriage return, a scheme-relative userinfo; CONTROL accepts for an ordinary name holding a tab, a credential value of only tab/newline characters, a cleaned scheme with no userinfo, a scheme-relative authority with no userinfo, an@in a scheme-relative path; the read rewrites at the matching authored offsets; refused ⇔ rewritten.driver-credential-refusal.test.ts— per URL-bearing driver key:user:password@refused with a tab, newline or carriage return inside the scheme or the password; CONTROL a bare username and a password of only that character; per query driver: the parameter refused with such a character inside its name, CONTROL a value of only such characters; the shared parses read pairs and userinfo with them deleted.datasource-credential-redaction.test.ts— the read removes the password at the mapped offsets and a credential pair whose name holds such a character, keeping the rest of the query; CONTROL strings unchanged.Dogfood, both save doors: connector — 422 for a credential name holding a tab, a scheme holding a tab, a scheme-relative userinfo; CONTROL an ordinary name holding a tab saves, materializes and is sent as the parser reads it, a scheme-relative URL with no userinfo saves. Datasource postgres — 422 for a
passwordname holding a tab and a scheme holding a tab; CONTROL a bare username and an ordinary name each with a tab save as written; a seeded row is served with its credential parts removed at the judged positions.Acceptance notes
Basis for the two additions beyond the ruled text. The maintainer's ruling D is "one value-level rule at both doors"; its stated purpose is that the check sees what the consuming parser sees. Removing tab/newline characters before judging, and the connector using the datasource's scheme-optional userinfo grammar, are that same rule. They narrow beyond the ruled literal text, and the seat has put that to the maintainer as a veto window.
Consumers. The cleaning matches the WHATWG parse that pg-connection-string, mysql2 and the mongodb driver apply to these values; for
@libsql/core, which applies none, it only refuses at publish a value the client rejects at connect.Cost measured. A non-URL string under
providerConfigholding a credential parameter with a value is now refused. Overexamples/,packages/apps/,skills/,content/docs/andapps/docs/(git grepfor a credential name after?or&with a value): 3 hits, none in a connector entry (a storage-service doc comment and two auth-docsfetchcalls); the three OpenAPI documentspackages/appsand the showcase reference by path: 0 hits.authendpoints are URLs by their own declared format, so a string with no scheme there is already refused by that format; the any-string rule matters underproviderConfig.The other package this diff touches is
packages/qa/dogfood(private, tests only).Verification
Head
49b7049edb(origin/mainat9edd6e783merged in).dispatch-gates --commandsderived 81 commands, each exit captured before any pipe;--ran: 81 derived, 81 run, 0 NOT-MEASURED.check:type-check-debtexit 0 (0 raw tsc errors).check:api-surface:public API surface + factory signatures unchanged.check:authorable-surface,check:strictness-ledger,check:spec-changes,check:upgrade-guide,check:dual-build-cjs-loadsexit 0.check:generated:All 14 generated artifacts are up to date.pnpm --filter @objectstack/spec test: 807 files, 24224 passed, 1 todo, exit 0. Two dogfood pin files 51/51. Fullpnpm --filter @objectstack/dogfood test: 410 passed and 3 skipped files, 3400 passed and 28 skipped tests, exit 0.scripts/ablation-replace.mjs, each restored to blob == HEAD withgit diff HEADempty: round 1 — leg A (no trim) 34/395 red, leg B (scheme gate on the query half) 6/395 red; round 2 — leg C (tab/newline removal removed) 51/463 red, split into query half only 13 and userinfo half only 38; leg D (scheme-optional authority removed) 9/463 red.49b7049edb: 55 success, 9 skipped, 0 failed.