Skip to content

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

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/sec-hardening-q
Nov 4, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/sec-hardening-q

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no value under skills/**, the hand-written content/docs/**, examples/** or packages/apps/** spells a special-scheme URL any way but scheme:// with an @ in it (repo-wide count at 1f13eb3189: 0), and the docs state the rule as "a user:password@ URL is refused", which reads the same after this change; each refusal carries its own prescription.

维护者速读

  • 改了什么:连接器实例与数据源驱动配置里的 URL 凭据判定,对特殊 scheme(http、https、ws、wss、ftp,大小写不限)按 URL 解析器的读法定位权限部分:scheme 之后无论跟几个 / 或 \(含零个)都整段跳过,再读 userinfo。两扇门共用同一个函数、同一套语法,写入拒绝与读取改写同步变化;其它 scheme 与 file: 不变,scheme:// 写法的读法不变。
  • 为什么改:维护者裁决(2026-11-04,第 4 批第 1 项,选 A):特殊 scheme 的权限部分按 WHATWG 的读法读取,两扇门一个函数一套语法;框架沿用裁决 D(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)——检查看到的就是解析器看到的。
  • 风险与回滚:破坏性收窄(spec minor,ADR-0087 D3 条目 url-credential-special-scheme-read-however-many-slashes)。升级后,特殊 scheme 之后不是恰好 //、且按解析器读法带凭据部分的 URL,在校验、构建产物加载与保存时被拒(保存门 422 INVALID_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.

  • Where a special scheme's authority starts. urlUserinfoSpan (packages/spec/src/data/driver/url-boundary.ts, internal, never re-exported) is the one userinfo grammar both doors read. For http, https, ws, wss and ftp, 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, keeps scheme:// or a scheme-relative //; file: carries no userinfo.
  • Both doors move together. The connector entry (connectorUrlWithoutUserinfo: the write's judgement and the read's rewrite) and the datasource doors (urlUserinfoPassword / urlUserinfoUsername at the write, redactUrlPassword at the read) call that one function. No second parser, and no new URL() at either door.
  • Where the authority ends is unchanged: at the first /, ? 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.
  • The module notes that stated the old start rule (connector-url-credentials.ts, connector-credential-redaction.ts, common.zod.ts, datasource-credential-redaction.ts) now state this one.
  • ADR-0087 D3 entry 18.url-credential-special-scheme-read-however-many-slashes.ts, in the family of 18.url-credential-judged-trimmed-and-in-any-connector-string.ts; changeset @objectstack/spec minor, BREAKING, with its FROM → TO (the three-slash spelling included) and the registration marker.
  • Unchanged. No public export added or removed; no .describe() text changed; file: and every scheme that is not special read as before; a URL spelled scheme:// 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 of http, 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): a user:password@ and a token userinfo are refused at providerConfig.baseUrl with a message that never echoes the value; the read rewrites the same URL minus the userinfo; the served form is accepted. The same at a providerConfig string 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, turso url, turso syncUrl): user:password@host refused after each special scheme × spelling; CONTROL a bare username and an @ in the path not refused for a password; no authority without // after file:, 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, turso syncUrl included: 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.
  • One-shot ablation (not committed): the special-scheme set emptied through scripts/ablation-replace.mjs (anchor 1 → 0, blob 71496e946da8 → 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: blob 71496e946da8 == HEAD, git diff HEAD empty; at HEAD the same three files are 1480/1480.

Acceptance notes

Verification

Head 8726545337, branched from origin/main at 1f13eb3189.

Gate, suite and generated-artifact readings at this head
  • Local gate subset — node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 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 — unbuilt dist/); --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-expressions exit 0, check:lean-entry-closure exit 0. Then check:dual-build-cjs-loads exit 0 (116 published require entry points across 71 packages load) and check:type-check-debt exit 0 (0 in the DEBT ledger). Final --ran: 81 derived, 81 run, 0 NOT-MEASURED.
  • Selected verdict lines from that run: 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-surface exit 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-typecheck OK, no new pinned signature.
  • Full pnpm --filter @objectstack/spec test at this head, under the verify lock — 811 files passed, 25480 passed | 1 todo, exit 0.
  • The three touched test files at this head: 1480/1480.
  • Dogfood battery (the title carries !), at 8726545337 under the verify lock: Test Files 417 passed | 3 skipped (420), Tests 3462 passed | 28 skipped (3490), exit 0.
  • CI: the PR's check-runs are the authority for every family CI runs; none was re-run locally beyond the subset above.

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 7 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/spec/src/data/datasource-credential-redaction.ts, packages/spec/src/data/driver/common.zod.ts, packages/spec/src/integration/connector-credential-redaction.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/connectors.mdx (via /api/v1/automation/connectors (route, a path literal in acceptanceCriteria), /meta/connector/:name (route, a path literal in surface))
  • content/docs/kernel/cluster.mdx (via /api/v1/automation/connectors (route, a path literal in acceptanceCriteria), /meta/connector/:name (route, a path literal in surface))

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

  • content/docs/releases/v15.mdx (via /api/v1/automation/connectors (route, a path literal in acceptanceCriteria))
  • content/docs/releases/v17/17-6.mdx (via /meta/connector/:name (route, a path literal in surface))
  • content/docs/releases/v18/18-3.mdx (via /api/v1/automation/connectors (route, a path literal in acceptanceCriteria))

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
  • 4 changed file(s) yielded no anchor (packages/spec/src/data/datasource-credential-redaction.ts, packages/spec/src/data/driver/common.zod.ts, packages/spec/src/integration/connector-credential-redaction.ts, …) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 170 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 a9fb14bdeeb5e09fec341ead4e0fffd8d8ed00bb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7f8b1461102fcec535fa99e9fc021c1c468a7a39 — the merge of head 87265453372a5c60d49f0031b7f004afac7cc3cc into base a9fb14bdeeb5e09fec341ead4e0fffd8d8ed00bb, 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 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

⚠️ 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 a9fb14bdeeb5e09fec341ead4e0fffd8d8ed00bb → 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

security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj

Contract-review-claim: session_01QSeKYn5CmmdShCJifm6WYj · head 8726545 · 2026-11-04T17:41Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 87265453372a5c60d49f0031b7f004afac7cc3cc
Local-runs: probe — one node run comparing new URL() against the head's urlUserinfoSpan over 55 strings (mixed-case special schemes, runs of one to five /, backslash-only and mixed runs, tab/LF/CR and surrounding spaces, file:, non-special schemes, scheme-less //, /\ and \\, plain text beginning http:); nothing built, no suite or gate re-run
Scope: packages/spec/src/data/datasource-credential-redaction.ts, packages/spec/src/data/driver/common.zod.ts, packages/spec/src/data/driver/url-boundary.ts, packages/spec/src/integration/connector-credential-redaction.ts, packages/spec/src/integration/connector-url-credentials.ts, packages/spec/src/migrations/entries/semantic/18.url-credential-special-scheme-read-however-many-slashes.ts, .changeset/url-credential-special-scheme-read-however-many-slashes.md, packages/spec/src/data/datasource-credential-redaction.test.ts, packages/spec/src/data/driver/driver-credential-refusal.test.ts, packages/spec/src/integration/connector-entry-url-credentials.test.ts + packages/services/service-datasource/src/datasource-config-redaction.ts, packages/services/service-datasource/src/datasource-credential-migration.ts, packages/services/service-datasource/src/datasource-admin-service.ts, packages/services/service-datasource/src/default-datasource-driver-factory.ts, packages/spec/src/data/datasource.zod.ts, packages/spec/src/kernel/metadata-type-redaction.ts, packages/spec/src/migrations/entries/semantic/18.url-credential-judged-trimmed-and-in-any-connector-string.ts, packages/spec/scripts/build-migration-registry.ts, packages/cli/src/utils/connection-display.ts

Inputs: the PR body and file list, the net diff against main at this head (10 files, +359/−20, branched from 1f13eb3189), the 65 check-runs on the head, PR #29875 (merged) as the precedent in the same family, and the published @libsql/core@0.18.0 tarball (lib-esm/uri.js, lib-esm/config.js) for the turso claim. Ruling of record: maintainer 2026-11-04, option A, under ruling D (#29875).

① Derived judgments

One grammar, one function, no new URL() at either door. The only grammar change is authorityStart() in url-boundary.ts: for a scheme in {ftp, http, https, ws, wss} (matched case-insensitively, SCHEME_RE is /i and the set lookup lowercases) the authority starts after the whole run of / and \ that follows scheme:, a run of zero included; every other string falls through to the unchanged AUTHORITY_START_RE (scheme:// or scheme-relative //). urlUserinfoSpan is the single reader; authorityStart, SCHEME_RE and AUTHORITY_SPECIAL_SCHEMES are module-private and url-boundary.ts is not re-exported, so the public surface is unmoved (check:api-surface unchanged on the head).

Per door, refusal ⇔ rewrite holds, by construction and by pin.

  • Connector write door: DeclarativeConnectorEntrySchema → connectorUrlCredentialPartsOf → connectorUrlWithoutUserinfo → urlUserinfoSpan. Connector read rewrite: connector-credential-redaction.ts → the same connectorUrlWithoutUserinfo. One function on both sides; the new block in connector-entry-url-credentials.test.ts pins refused ⇔ rewritten over 35 special leads plus 15 no-authority controls, each at three values and two positions (providerConfig.baseUrl and a non-URL providerConfig string), with the served form re-parsed green.
  • Datasource write door: credentialFreeUrl on postgres/mysql/mongo/turso config.url and turso config.syncUrl → urlUserinfoPassword → urlUserinfoSpan. Datasource read rewrite: redactDatasourceConfig → redactUrlCredentials → redactUrlPassword → urlUserinfoSpan, which removes the password and keeps the username, so the served form passes the write door. Pinned per key, syncUrl included, in datasource-credential-redaction.test.ts (refused ⇔ rewritten over the special and no-authority leads) and driver-credential-refusal.test.ts (refusal pathed at the key, bare-username and path-@ controls, // forms keep their reading).
  • Unchanged, read and confirmed by the probe: file: (not in the set; file: with no // has no authority, file:/// an empty one; file://u:p@h reads as it did on main), every non-special scheme (postgres:, postgresql:, mysql:, mongodb:, libsql:, mailto:, https+x:), the scheme-relative // form, and every scheme:// spelling (the special branch skips exactly two characters there, the same offset AUTHORITY_START_RE gave). The narrowing is exactly: a special scheme followed by zero, one, or three-or-more slashes/backslashes, or any run holding a \, now reads its authority.

Authority end unchanged: rest.search(/[/?#]/) is untouched; \ still does not end it. The libsql claim is verified against the published @libsql/core@0.18.0: its URI regex takes the authority as an optional // followed by a run of characters that are none of /, ? and #, and its authority regex takes the userinfo as a username of non-colon characters, an optional colon and a greedy password, then an @ — so the libsql client ends the authority at /, ?, # only and its userinfo at the last @ — the longer authority the head keeps holds every userinfo either parser reads, and a \-ended reading would hold fewer. Two further libsql facts bear on the turso keys: it requires the // (encodeBaseUrl refuses a URL with no authority) and lowercases the scheme before routing, so no turso datasource spelled a special scheme without exactly // ever connected; the narrowing takes away no working turso value.

service-datasource restore and migration detectors. getDatasource() serves through redactDatasourceConfig, so a stored row holding such a value under a URL key is now served with its password removed and the key named in redactedConfigKeys; restoreRedactedConfig is derived from the same redactor (it grafts the stored leaf back wherever the patch equals the served projection), so a save of the served form keeps the stored value and nothing is silently deleted; the generic metadata door's metadata-type-redaction.ts and redactedPathsCarriedForward are the same derivation. planCredentialMigration → urlCredentialKeys (redactUrlCredentials(value) !== value) now refuses such a row with the per-row remedy instead of planning nothing-to-migrate. This is right: ruling D makes the read rewrite exactly what the write refuses and never re-judges a stored row, and the restore is what keeps the second half true. The two connect-path consumers of the username half (buildMongoAuth → urlUserinfoUsername, and the mongo "names no user" refinement in datasource.zod.ts) read mongodb: only, which is not special — unchanged.

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; hunter2 is the pre-existing placeholder of both driver test files on main.

② Semver level

@objectstack/spec minor, title feat(spec)!:, Clause-②: no (narrowing) and Metadata-protocol: narrowed on both the PR body and the changeset — correct: a narrowed kind takes at least minor on the moved package and a narrowing is BREAKING. No other package publishes a change.

D3 entry packages/spec/src/migrations/entries/semantic/18.url-credential-special-scheme-read-however-many-slashes.ts: id equals the filename stem; the directory is the registry (loadEntries reads it with readdirSync, there is no index), and the changeset carries the marker adr-0087: registered url-credential-special-scheme-read-however-many-slashes; Lint & Repo Gates (carries check:adr-0087-registration) and TypeScript Type Check (carries check:spec-changes / check:upgrade-guide) are success on the head, so it is registered and projects. Its surface, replacement and acceptanceCriteria are the family's (18.url-credential-judged-trimmed-and-in-any-connector-string.ts) narrowed to the special schemes; its reason names all four spellings, the triple-slash one included. The changeset FROM/TO: FROM spells the triple-slash form (https:///billing:…@api.billing.example.com/v1), TO the scheme:// URL with the secret under auth: { type, credentialRef }, and the sentence under it extends the same mapping to the no-slash, one-slash and backslash spellings and to the datasource keys. Both declaration lines are first on the changeset body as the gate reads them.

③ Boundary flags

  • Scheme-less leading pair mixing / and \ (dev's open question): option A, leave as ruled. A scheme-less string has no WHATWG reading at all without a base (new URL() fails //u:p@h, /\u:p@h and \\u:p@h alike); the // reading both doors keep is RFC 3986's network-path reference, in which \ is not a delimiter, and neither door's consumer resolves a scheme-less value against a special base (fetch needs an absolute URL, libsql needs a scheme). The ruling names the special schemes only; reading /\ as scheme-relative would be a new ruling, not an execution of this one. Nothing slips meanwhile.
  • Residual (2), packages/cli/src/utils/connection-display.ts redactConnectionUrl: a print-time label in os dev / os start / os serve that reads the scheme:// grammar and new URL(); it is not one of the two doors the ruling names and is outside this PR's package, so "no other package is touched" is correct scope. It is a follow-up — and because it concerns a credential reaching a log line it is not a public card: it stays with the security#1 seat under security-findings.md §5/§9 (private field, no-card dispatch). Not a finding on this PR.
  • Verification section written while three readings were pending. The readings are now in on this head: dogfood battery 417 files / 3462 tests green; check:dual-build-cjs-loads exit 0; check:type-check-debt exit 0; --ran 81/81 accounted for. The check-runs are the gate verdicts and every gate-carrying context is success (the ten required contexts included), so landing is not blocked by the body. Should-fix: before landing, the seat corrects the body's Verification block to carry those four readings against head 8726545337 (edit the body through the fleet path, read it back, keep the footer as stored), so the PR of record names its readings rather than deferring them to a comment.

④ Design judgment

authorityStart() against WHATWG. The WHATWG special-authority-slashes and special-authority-ignore-slashes states skip every / and \ after scheme: for a special scheme, zero included, then enter the authority state; the head's loop is that reading. Probe (55 strings, no suite): mixed-case schemes (HtTpS:\\, WSS:, FTP:///, HTTP:U:P@H), four and five slashes, a backslash-only run of four, mixed runs (https:/\/\, http:/\/), tab inside the scheme, LF after the colon, CR before //, surrounding spaces — all read the same userinfo new URL() reads. The last-@ rule matches both WHATWG (https:u:p@h@i → u:p%40h) and libsql. No string WHATWG reads as credentials is missed (GRAMMAR_LESS 0 of 55). Every divergence is in the refusing direction and was already so on main for the // spelling: an authority WHATWG ends at \ (https://h\path@x) and an empty userinfo (https:@h, which the connector door refuses as it refuses https://@h; the datasource door accepts both, no password). A percent-encoded @ is a delimiter for neither.

Can a string WHATWG reads as credentials still slip past either door? None found. The doors judge the cleaned string (urlAsParsed), so a tab, LF, CR or surrounding C0/space cannot hide the scheme or the run; the scheme regex is WHATWG's character class; file is the one special scheme left out, and WHATWG itself fails a file:// URL holding a userinfo. The narrowing makes the spec's own refusal real at the exact spellings a URL parser reads credentials from, at the consumer (fetch, new URL()) that would read them.

Can a string that is not a URL be falsely refused? A plain-text providerConfig or auth string that begins http:, https:, ws:, wss: or ftp: and holds an @ before any /, ? or # — http:contact admin@example.com — is now refused. That is what the ruling orders: WHATWG reads that string as userinfo contact%20admin at host example.com. The refusal is located at the string, names the parts and never the value, and the author's fix is not to open a note with a scheme. The repo-wide count at 1f13eb3189 is 0; an email with no scheme, a mailto: string, a non-special scheme and file: are untouched (probe). Accepted consequence of ruling D, not a finding.

Findings: must-fix none; should-fix one (③, the Verification block).

Implemented-by: claude/sec-hardening-q
Reviewed-by: session_01QSeKYn5CmmdShCJifm6WYj

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj

席位核对

ACCEPT · head 87265453372a5c60d49f0031b7f004afac7cc3cc · 2026-11-04T18:00Z

  • Shape: 10 files, +359/−20. Not governed. Under 3000. Minor for @objectstack/spec, with !, Clause-②: no (narrowing), Metadata-protocol: narrowed, and ADR-0087 registered url-credential-special-scheme-read-however-many-slashes.
  • Rule held (security-channel ruling of 2026-11-04, option A, under ruling D of 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): a URL with a special scheme is read for credentials the way a URL parser reads it, however many slashes follow the scheme.
    • Both doors keep one function and one grammar; no second parser is added.
    • At every door, a refused value is also rewritten on read, and only a refused one.
    • file:, non-special schemes and // forms are unchanged.
  • Contract review: comment 6637986042. Served-tier: CONTRACT_REVIEW_TIER. Head-sha equals this head. VERDICT PASS, no must-fix. Open question answered A: a scheme-less mixed slash pair stays as ruled.
  • Check-runs at this head (latest per name): 11 skipped, 53 success. mergeable_state: clean.
  • Gates (dev):
    • dispatch-gates --ran: 81 derived, 81 run, 0 NOT MEASURED.
    • Full spec suite: 25480 passed.
    • Full dogfood battery: 417 files, 3462 tests.
    • Ablation: 406 red, 0 controls red, restored.
  • Seat edit: per the review's should-fix, the body's Verification block now carries the four readings that came in after it was written.
  • Follow-up (not public, the seat's own order): the CLI connection label redaction, which reads only the scheme:// form.
  • Landing: ready, then auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 4, 2026 18:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 63a470a Nov 4, 2026
69 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/sec-hardening-q branch November 4, 2026 18:34
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 protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant