Repository navigation
feat(spec)!: a URL with a special scheme is read for credentials the way a URL parser reads it, however many slashes follow the scheme - #30149
Conversation
…way a URL parser reads it, however many slashes follow the scheme The shared userinfo grammar starts the authority of an `http`, `https`, `ws`, `wss` or `ftp` URL (any letter case) after the whole run of `/` and `\` that follows the scheme, none included, as a URL parser does; every other scheme keeps `scheme://` or a scheme-relative `//`. Both doors read the one grammar, so the write refusal and the read rewrite move together. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…rs however many slashes follow the scheme Per special scheme and form (no slash, one slash, a backslash, three slashes, a mixed run, an uppercase scheme): refused at the write door and rewritten by the read at the judged position, at the connector entry and at every URL-bearing driver key, turso `syncUrl` included. CONTROL: `file:`, schemes that are not special, and the `//` forms keep their reading. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
… read for credentials however many slashes follow the scheme 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:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 170 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 7f8b1461102fcec535fa99e9fc021c1c468a7a39 && git checkout 7f8b1461102fcec535fa99e9fc021c1c468a7a39
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a9fb14bdeeb5e09fec341ead4e0fffd8d8ed00bb 87265453372a5c60d49f0031b7f004afac7cc3cc && git checkout -B drift-repro a9fb14bdeeb5e09fec341ead4e0fffd8d8ed00bb && git merge --no-ff 87265453372a5c60d49f0031b7f004afac7cc3cc
node scripts/docs-audit/affected-docs.mjs --json a9fb14bdeeb5e09fec341ead4e0fffd8d8ed00bb
|
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract-review-claim: session_01QSeKYn5CmmdShCJifm6WYj · head 8726545 · 2026-11-04T17:41Z |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract reviewServed-tier: Inputs: the PR body and file list, the net diff against ① Derived judgmentsOne grammar, one function, no Per door, refusal ⇔ rewrite holds, by construction and by pin.
Authority end unchanged: service-datasource restore and migration detectors. Disclosure (security-findings.md §6). The title, body, changeset, D3 entry, the three commit messages and every test name state the rule ("read for credentials the way a URL parser reads it, however many slashes follow the scheme"); a grep of all of them and the full diff for mechanism vocabulary returns nothing. The changeset's FROM → TO is the migration mapping a breaking changeset must carry (same form as #29875's). Placeholders are fake; ② Semver level
D3 entry ③ Boundary flags
④ Design judgment
Can a string WHATWG reads as credentials still slip past either door? None found. The doors judge the cleaned string ( Can a string that is not a URL be falsely refused? A plain-text Findings: must-fix none; should-fix one (③, the Verification block). Implemented-by: VERDICT: PASS |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj 席位核对ACCEPT · head
|
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no value under
skills/**, the hand-writtencontent/docs/**,examples/**orpackages/apps/**spells a special-scheme URL any way butscheme://with an@in it (repo-wide count at1f13eb3189: 0), and the docs state the rule as "auser:password@URL is refused", which reads the same after this change; each refusal carries its own prescription.维护者速读
http、https、ws、wss、ftp,大小写不限)按 URL 解析器的读法定位权限部分:scheme 之后无论跟几个/或\(含零个)都整段跳过,再读 userinfo。两扇门共用同一个函数、同一套语法,写入拒绝与读取改写同步变化;其它 scheme 与file:不变,scheme://写法的读法不变。url-credential-special-scheme-read-however-many-slashes)。升级后,特殊 scheme 之后不是恰好//、且按解析器读法带凭据部分的 URL,在校验、构建产物加载与保存时被拒(保存门 422INVALID_METADATA,定位到该字符串,不回显值)。仓内 examples/apps/skills/docs 实测 0 处受影响。已存行不重判;读取隐去凭据部分,原样保存保留库值。回滚即 revert 本 PR。packages/spec/src/data/driver/url-boundary.ts(authorityStart:特殊 scheme 分支,唯一的语法改动)、packages/spec/src/integration/connector-entry-url-credentials.test.ts(新块:逐 scheme × 写法的拒绝、改写与对照)、.changeset/url-credential-special-scheme-read-however-many-slashes.md(FROM → TO)。pnpm --filter @objectstack/spec exec vitest run src/integration/connector-entry-url-credentials.test.ts src/data/driver/driver-credential-refusal.test.ts src/data/datasource-credential-redaction.test.ts,预期 3 个文件 1480 个用例全部通过。What changes
One rule, read the way a URL parser reads it — a URL with a special scheme is read for credentials the way a URL parser reads it, however many slashes follow the scheme.
urlUserinfoSpan(packages/spec/src/data/driver/url-boundary.ts, internal, never re-exported) is the one userinfo grammar both doors read. Forhttp,https,ws,wssandftp, in any letter case, it now starts the authority after the whole run of/and\that follows the scheme, however long, none included (authorityStart). Every other scheme, and a string with no scheme, keepsscheme://or a scheme-relative//;file:carries no userinfo.connectorUrlWithoutUserinfo: the write's judgement and the read's rewrite) and the datasource doors (urlUserinfoPassword/urlUserinfoUsernameat the write,redactUrlPasswordat the read) call that one function. No second parser, and nonew URL()at either door./,?or#. A URL parser also ends a special scheme's authority at\; an RFC 3986 parser (the libsql client's, which reads turso URLs) does not, so the longer authority is kept — it holds every userinfo either parser reads.connector-url-credentials.ts,connector-credential-redaction.ts,common.zod.ts,datasource-credential-redaction.ts) now state this one.18.url-credential-special-scheme-read-however-many-slashes.ts, in the family of18.url-credential-judged-trimmed-and-in-any-connector-string.ts; changeset@objectstack/specminor, BREAKING, with its FROM → TO (the three-slash spelling included) and the registration marker..describe()text changed;file:and every scheme that is not special read as before; a URL spelledscheme://reads as before; a stored row is never re-judged — a save of its served form keeps the stored value, and an edit of the served value stores the edit.Tests
Test names state the rule. Every placeholder is fake and every assertion on one is its absence from what a door served.
packages/spec/src/integration/connector-entry-url-credentials.test.ts— new block. For each ofhttp,https,ws,wss,ftp× seven spellings of the run after the scheme (none, one slash, a backslash, three slashes, a run of slashes and backslashes, an uppercase scheme with none, an uppercase scheme with three slashes): auser:password@and a token userinfo are refused atproviderConfig.baseUrlwith a message that never echoes the value; the read rewrites the same URL minus the userinfo; the served form is accepted. The same at aproviderConfigstring that is not the base URL. CONTROL: no userinfo, or an@only in the path, accepted and untouched;file:,postgres:,libsql:with no//hold no authority;file:///,postgres:///,libsql:///hold an empty one; the//forms keep their reading. Refused ⇔ rewritten over every case and control.packages/spec/src/data/driver/driver-credential-refusal.test.ts— per URL-bearing driver key (postgres, mysql, mongo, tursourl, tursosyncUrl):user:password@hostrefused after each special scheme × spelling; CONTROL a bare username and an@in the path not refused for a password; no authority without//afterfile:,postgresql:,mysql:,mongodb:,libsql:; the//forms keep their reading. The shared parse (urlUserinfoPassword,urlUserinfoUsername) reads the special scheme's userinfo, on the cleaned string too.packages/spec/src/data/datasource-credential-redaction.test.ts— per URL key, tursosyncUrlincluded: refused at the write door, served with the password removed and the username kept, the served form passes the write door; refused ⇔ rewritten over the special and the no-authority spellings; CONTROL a string with no authority served as stored.scripts/ablation-replace.mjs(anchor 1 → 0, blob71496e946da8→008323104982), the mutation held only inside the verify lock, with a trap restore. The three files: 406 failed | 1074 passed (1480) — every failure a refusal, rewrite or parse pin of the new rule, no CONTROL among them; the refused ⇔ rewritten pins stay green under it, as they must (they pin that the two doors agree, which one shared function holds whatever the grammar reads). Restored: blob71496e946da8== HEAD,git diff HEADempty; at HEAD the same three files are 1480/1480.Acceptance notes
git grepat1f13eb3189(CHANGELOGs included) for a special-scheme string not spelledscheme://that holds an@before its first/,?or#: 0 hits. The pattern matches each of the spellings above on a control file and excludesscheme://,mailto:and an@in a path./,?,#(What changes, third bullet): reading\as the end would accept values the libsql client reads a userinfo from; the longer authority only ever refuses more, never less.DeclarativeConnectorEntrySchemaand the driver config schemas these tests exercise; 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's dogfood pins cover that wiring. No dogfood pin added here.packages/specand one changeset.Verification
Head
8726545337, branched fromorigin/mainat1f13eb3189.Gate, suite and generated-artifact readings at this head
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 81 commands from this diff; each ran with its exit captured before any pipe. First pass: 77 exit 0, 4 exit 3 (PREREQUISITE NOT MET — unbuiltdist/);--ran:81 derived famil(ies) accounted for — 77 run, 4 NOT-MEASURED. After building the dogfood closure (turbo run build --filter='@objectstack/dogfood^...', 69/69):check:doc-formula-expressionsexit 0,check:lean-entry-closureexit 0. Thencheck:dual-build-cjs-loadsexit 0 (116 published require entry points across 71 packages load) andcheck:type-check-debtexit 0 (0 in the DEBT ledger). Final--ran: 81 derived, 81 run, 0 NOT-MEASURED.check:api-surface—@objectstack/spec public API surface + factory signatures unchanged;check:adr-0087-registration—1 declared-breaking changeset(s), each carrying an ADR-0087 disposition(registered url-credential-special-scheme-read-however-many-slashes);check:spec-changes/check:upgrade-guide— generate from the registries in memory;check:authorable-surfaceexit 0 (authorable-defaults/ … 1102 default(s) unchanged);check:nul-bytes— no raw ASCII control bytes.pnpm --filter @objectstack/spec check:generated—All 14 generated artifacts are up to date.pnpm --filter @objectstack/spec typecheck— exit 0;check:test-typecheckOK, no new pinned signature.pnpm --filter @objectstack/spec testat this head, under the verify lock — 811 files passed, 25480 passed | 1 todo, exit 0.!), at8726545337under the verify lock: Test Files 417 passed | 3 skipped (420), Tests 3462 passed | 28 skipped (3490), exit 0.