Uh oh!
There was an error while loading. Please reload this page.
feat(spec)!: refuse URL-embedded credentials (user:password@host) in driver config.url at publish (#8082) - #8341
Conversation
…publish (#8082) The #7990 closure refused the inline credential keys; #8078 measured and pinned that config.url still accepted the identical secret one syntax over. Maintainer-ruled Option A (2026-08-12): one value-level parse (urlUserinfoPassword / credentialFreeUrl, data/driver/common.zod.ts) shared by the four URL-bearing driver schemas — postgres/mysql/mongo url, turso url + syncUrl — refuses a URL whose userinfo carries a non-empty password. - Bare-user userinfo (user@host) stays accepted, matching #7990's posture (username is a writable key; only the secret is refused) — and it is the exact shape the #8126 read path serves for legacy rows, so an untouched Save keeps working. - The refusal message names the working mechanisms (secret binder / external.credentialsRef), states the runtime-DSN carve-out explicitly (OS_DATABASE_URL never passes the publish door), and warns that ${...} placeholders resolve to nothing (#8078, measured) instead of steering authors into that broken escape. - The #8078 acceptance pin (driver-credential-refusal.test.ts) is INVERTED to a rejection pin, not deleted; the #8126 write-door acceptance pin in service-datasource flips the same way (its own comment said it waited on exactly this ruling). - ADR-0087: D3 semantic entry datasource-config-url-userinfo-refused (no D2 conversion — a credential cannot be mechanically encrypted into sys_secret); registry, spec-changes, upgrade guide, api-surface, export-origins, reference docs regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2
…ixture triage) mongo.test.ts / postgres.test.ts 'accept config with connection URI' merely used the userinfo spelling to demonstrate URI acceptance — re-spelled to the bare-user form the rule still accepts (disposition: re-spell; the family rejection pins live in driver-credential-refusal.test.ts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2
…-userinfo-publish-refusal
…egen step 4) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
…tree (#8116 x #8341/#8335) Discharges the os-regen deferral from the preceding merge commit: the data.json shards regenerate with both sides present — this PR's 10 new provenance exports and main's landed entries (zodIssuesToFields intact in the api.json shards). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2
…gain (#8315) Third lap of merge-then-regenerate; main moved 25 commits while CI was green and the arm was held. The conflict-free merge again left the two driver-managed prose projections stale, this time missing `datasource-config-url-userinfo-refused` (#8082 / PR #8341): present in origin/main's copies, absent after the merge, restored by regeneration. Round-2's prediction held exactly. `registry.ts` is byte-identical to the plain merge (79 semantic entries) because it is NOT_DRIVER_MANAGED and #8341 added a NEW entry file — the #7297 per-entry-file sharding removes the conflict at the source. The two `merge=os-regen` projections are the ones that needed the generator: spec-changes.json +14 lines, protocol-upgrade-guide.md +3. Regeneration only — no code change, no changeset reclassification, no ratchet. Merge committed first, then: pnpm --filter @objectstack/spec gen:migration-registry pnpm --filter @objectstack/spec gen:spec-changes pnpm --filter @objectstack/spec gen:upgrade-guide Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEVB6w7D7uCszR9Mw1BL73
Fixes#8082
Implements the maintainer-ruled Option A (on-card comment, 2026-08-12): driver
config.urlcarrying userinfo credentials (user:password@host) is refused at publish, via one value-level parse shared by the URL-bearing driver schemas.What changed
packages/spec/src/data/driver/common.zod.ts):urlUserinfoPassword()extracts the userinfo password segment using RFC 3986 boundaries (authority after//up to/?#; userinfo ends at the LAST@; password after the FIRST:), deliberately notnew URL()— real DSN forms (multi-hosth1:5432,h2:5432,:memory:,file:paths) are judged instead of failing open.credentialFreeUrl()attaches the refusal to a URL key;URL_EMBEDDED_CREDENTIAL_REFUSEDis the message.config.url, tursoconfig.urlandconfig.syncUrl(same authored-URL cleartext sink, same shared parse — one small extension beyond the foururlkeys, flagged for review).user@hostanduser:@hoststay accepted — follows [security]sys_metadata.metadatais a general cleartext sink: any authored artefact whose schema permits an inline credential lands it there (datasourceconfig.password, connectorauthentication) #7990's posture (usernameis a writable inline key; only the secret is refused). Only a NON-EMPTY password segment is credential material. This also keeps the fix(service-datasource): stop serving stored credentials on the datasource read path, and fix the false "credential-stripped" claim (#8081) #8126 legacy-row round-trip working: the read path servesuser@hostfor a storeduser:pass@hostrow, and that shape must keep parsing green or every untouched "Save" would 400 (the regression shape reported on [security]sys_metadata.metadatais a general cleartext sink: any authored artefact whose schema permits an inline credential lands it there (datasourceconfig.password, connectorauthentication) #7990).sys_secret+external.credentialsRef); states the runtime-DSN carve-out explicitly (OS_DATABASE_URLand friends never pass through the publish door — unaffected by construction); warns that${…}placeholders are resolved by nothing and reach the client verbatim (feat(spec)!: refuse inline credentials at publish — driver config + connector authoring door (#7990, spec half) #8078, measured) instead of steering authors into that broken escape.driver-credential-refusal.test.ts, formerly lines 166-174, located by content): same input, opposite verdict, asserting the zod issue'scode(custom) + re-pathed location (config.url) + the message contract. Note oncode+status: an HTTPstatusdoes not exist at the schema layer — every schema refusal is wrapped uniformly by the publish door (metadata-protocol's422 INVALID_METADATAenvelope, whoseissues[]carry these zod codes verbatim); the pin asserts the full envelope the schema layer carries.packages/services/service-datasource/src/__tests__/datasource-config-redaction.test.ts): its own comment said "this pin fails the moment that boundary moves without a ruling" — the ruling arrived, so it now asserts the refusal plus the continued acceptance of the redacted shape. The redaction module's header no longer claims the refusal is unruled. These two files are outside the dispatched file surface (packages/spec/src/data/driver/**); they are in the narrowed rule's consumption radius and would have gone red on merge — flagged here per the fixture-sweep clause rather than silently left red.datasource-config-url-userinfo-refused(no D2 conversion — a credential cannot be mechanically encrypted intosys_secret; auto-stripping userinfo would silently drop a live credential). Registry, spec-changes, upgrade guide, api-surface, export-origins, and reference docs regenerated viacheck:generated --fix(exactly the five proved stale).@objectstack/specmajor (rc pre-mode absorbs it, same as feat(spec)!: refuse inline credentials at publish — driver config + connector authoring door (#7990, spec half) #8078), with FROM → TO and the registered marker.Test coverage
Per URL key (5 surfaces × the family suite): refusal of
user:password@,${…}-placeholder password, percent-encoded password, malformed double-@password (caught WHOLE at the RFC 3986 boundary, mirroring the read-path redactor), userinfo before an IPv6 host; acceptance of bare-user, credential-free, and IPv6-without-userinfo forms byte-identically. Detector unit tests cover multi-host DSNs, scheme-relative URLs, path/query/fragment colons,file:/:memory:/bare strings, and the empty-password boundary.Reverse verification (fix committed first): restoring the pre-ruling
urllines flips the new rejection pins red while the acceptance pins stay green — direction as predicted; readings in the dev report on #8082.Carve-out, verified on the tree
OS_DATABASE_URL/ CLI / standalone-stack DSNs are translated into driver configs and handed to the driver factory directly (packages/runtime/src/standalone-stack.ts,packages/cli/src/utils/storage-driver.ts); they never meet the authoring schema — the same construction #8078 already relies on for the tursoauthTokenenv path. The refusal docs state this rather than implying it, per the ruling.Out of scope, tracked separately (both remain open, neither is addressed here): the
${…}placeholder escape, tracked as #8336; the turso?authToken=query-parameter door, tracked as #8337. #8081's stored-cleartext remediation is likewise untouched (#8126 already narrowed read exposure; the ruling calls for no sweep here).Generated by Claude Code