Repository navigation
fix(cli): os serve refuses an OS_DATABASE_URL whose scheme no driver recognises, and reads pg:// and a scheme-less path as the runtime host does - #23761
Conversation
…, and classify pg:// and scheme-less paths as the runtime host does inferDriverTypeFromUrl answered '' for an unrecognised `<scheme>://` (redis://...), so `serve --dev` on the legacy path booted the SQLite :memory: default in silence and production met the no-storage refusal. It now throws UnsupportedDriverError (recognized: false) naming the scheme and the recognised set, and takes the runtime host's arms for pg:// (postgres) and a value with no scheme (a SQLite file path). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
…eement with the runtime host, in dev and prod Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
…eset for the URL-scheme refusal Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
…recognised-database-url-scheme
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 28 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 378f539b4439ed060e0edfee7ef189e4b5771719 && git checkout 378f539b4439ed060e0edfee7ef189e4b5771719
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6362c66ca89bb4211e9a9a750cec4de3b62b2dcf 40fe66778a356be95d7a6e3118cdbd128ff60d6c && git checkout -B drift-repro 6362c66ca89bb4211e9a9a750cec4de3b62b2dcf && git merge --no-ff 40fe66778a356be95d7a6e3118cdbd128ff60d6c
node scripts/docs-audit/affected-docs.mjs --json 6362c66ca89bb4211e9a9a750cec4de3b62b2dcf |
🔁 REWORK: PR #23761 at
|
…te:/sqlite:// with the runtime host's normalizeDatabaseUrl A value whose scheme is two or more characters and a colon with no `//` (jdbc:postgresql://..., redis:h) is refused with the same UnsupportedDriverError and sentence, naming the scheme only; a one-letter drive (C:\data\app) stays a path. The sqlite arm now derives its file through normalizeDatabaseUrl, so sqlite://x.db opens x.db, not //x.db. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
… both hosts; changeset for both Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
…nd the sqlite-wasm arm takes the same alias rule A `.wasm.db` ending was tested ahead of every scheme check, so it rescued a scheme nothing recognises (redis://h/x.wasm.db, jdbc:x/x.wasm.db) into a SQLite file. It now applies to a file:/sqlite: URL or a scheme-less value only. The sqlite-wasm arm derives its file through normalizeDatabaseUrl as the sqlite arm does, so sqlite://x.wasm.db opens x.wasm.db. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
… URL-scheme changeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
✅ ACCEPT (round 1): PR #23761 at
|
Fixes #23633
Clause-②: no
What this changes
inferDriverTypeFromUrl(packages/cli/src/utils/storage-driver.ts) is howos serveclassifiesOS_DATABASE_URLon its legacy path. A host config takes that path (one whosepluginshold plugin instances), and so doesOS_MODE=off|none|legacy.os devandos startreach it through theirservechild. The function answered''for any URL its regexes missed.SCHEME://(redis://…) is now refused. The function throwsUnsupportedDriverError(recognized: false), as it already does formemory://andmingo://. The message namesOS_DATABASE_URL, the scheme, and the recognised spellings in the runtime host's order. It names the scheme only, never the URL, so credentials in the URL are not printed. serve.ts callsresolveDriverTypeabove its storagetry, so the throw reaches the fatal handler and the boot exits 1 in dev and production. serve.ts is not edited.pg://is postgres, and a value with no scheme is a SQLite file path, whatever its extension, as indetectDriverFromUrl(packages/runtime/src/standalone-stack.ts, read only). An OPAQUE scheme (two or more characters and a:with no//, such asjdbc:postgresql://h/dborredis:h) is refused with the sameUnsupportedDriverErrorand sentence, naming the scheme ("jdbc:"). The runtime host still opens those as a SQLite file at a path made of the URL, against its own rule that an unknown scheme never falls back to SQLite; that gap is runtime(standalone host): anOS_DATABASE_URLwith an opaque scheme (jdbc:postgresql://…,redis:h) is opened as a SQLite file path and creates directories, against the module's own "unknown URL schemes throw" #23765's. A one-letter drive is a path, not a scheme (C:\data\app,C:/data/app)..dbor.wasm.dbending no longer rescues an unrecognised scheme. The old*.dbarm ran before any scheme check, soredis://h/app.dbwas handed to SQLite as a file path. It is now refused, as the runtime host refuses it. A*.wasm.dbname likewise selects the WASM engine only for afile:/sqlite:URL or a path with no scheme, soredis://h/app.wasm.dbandjdbc:x/app.wasm.dbare refused too. The runtime host still acceptsredis://h/x.wasm.dbas a WASM SQLite file (measured below); the seat records that host's answer on runtime(standalone host): anOS_DATABASE_URLwith an opaque scheme (jdbc:postgresql://…,redis:h) is opened as a SQLite file path and creates directories, against the module's own "unknown URL schemes throw" #23765.sqlite:andsqlite://are aliases offile:, by the runtime host's own rule. The sqlite and sqlite-wasm arms ofresolveStorageDefinitiontranslate the URL throughnormalizeDatabaseUrl(@objectstack/runtime) rather than a second copy of it.sqlite://x.dbandsqlite:x.dbopen./x.db,sqlite:///abs/x.dbopens/abs/x.db, andsqlite://x.wasm.dbopens./x.wasm.dbon the WASM engine. Before,sqlite://x.dbopened//x.db, a file at the filesystem root.''. Without that guard the new scheme-less arm would read them as a file path.OS_DATABASE_DRIVERstill wins.resolveDriverTypedoes not classify the URL at all when a driver is named.The
memory:///mingo://refusal and every recognised spelling resolve exactly as before. No export is added, andpackages/specis not touched.Measured at the public door
Built CLI (
node packages/cli/bin/run.js),NODE_ENVunset,OS_CLOUD_URL=off, auth secret set. The fixture is a throwawaydefineStackhost config: one object and one instantiated no-op plugin. Before = base3b46855fb; after = this branch at54933125a, built.OS_DATABASE_URL=redis://127.0.0.1:6399/0 os serve --dev✓ Server is ready, bannerDriver: SqlDriver(better-sqlite3) → :memory:✗ Unsupported database URL scheme "redis://" (OS_DATABASE_URL / --database). Supported: …os serve)os dev --no-watch, same URL🗄️ Database: redis://127.0.0.1:6399/0, then the banner→ :memory:os start, same URLredis://admin:PASSWORD@127.0.0.1:6399/0 os serve --dev→ :memory:pg://u:p@127.0.0.1:5999/db,--dev→ :memory:datasource 'default': connect failed — connect ECONNREFUSED 127.0.0.1:5999(the URL is used)./data/app,--dev→ :memory:✓ Server is ready, banner→ ./data/app; the file exists./data/app2, production✓ Server is ready, banner→ ./data/app2memory://,--devpostgres://u:p@127.0.0.1:5999/db,--devfile:./ctl.db, production✓ Server is ready,→ file:./ctl.dbOS_DATABASE_DRIVER=postgres+redis://127.0.0.1:6399/0,--devredis://…,--dev[StandaloneStack] Unsupported database URL scheme: …Round 1, measured the same way
Before =
683bf9587(round 0's head;storage-driver.tsis the only non-test source file between the two); after =40fe66778. Each boot ran in a throwaway directory, so every file it wrote is listed relative to it.OS_DATABASE_URLjdbc:postgresql://127.0.0.1:5999/db✓ Server is ready,→ jdbc:postgresql://127.0.0.1:5999/db; created./jdbc:postgresql:/127.0.0.1:5999/db✗ Unsupported database URL scheme "jdbc:" (OS_DATABASE_URL / --database). Supported: …; no fileredis:h✓ Server is ready,→ redis:h; created./redis:h"redis:"; no filesqlite://x.db//x.db, the filesystem root (read from the built module, not booted: a boot writes/x.db)✓ Server is ready,→ sqlite://x.db; created./x.dbredis://h/x.wasm.db✓ Server is ready,SqliteWasmDriver → redis://h/x.wasm.db; created./redis:/h/x.wasm.db"redis://"; no fileC:/data/app(read from the built module, no boot)C:/data/apppostgres://u:p@127.0.0.1:5999/dbfile:./ctl.db,--dev✓ Server is ready,→ file:./ctl.dbThe runtime host, read and booted for
redis://h/x.wasm.dband not changed here:resolveStandaloneDatabaseanswerssqlite-wasmon the fileredis://h/x.wasm.db, and a plain config boots to✓ Server is readyin dev and production with a 1,499,136-byte database at./redis:/h/x.wasm.db.The two hosts, URL by URL
The runtime host's verdict is
resolveStandaloneDatabasefrom@objectstack/runtime. The CLI host's isresolveStorageDefinition(resolveDriverType(undefined, url), …). Only the rows that moved are listed; every other row (postgres://,postgresql://,mysql://,mysql2://,mongodb://,mongodb+srv://,libsql://,https://*.turso.io,wasm-sqlite://, a scheme-less orfile:*.wasm.db,memory://,mingo://,file:,sqlite:,:memory:,*.db,*.sqlite,*.sqlite3) was already equal and is unchanged.OS_DATABASE_URLredis://…,REDIS://…,rediss://…,http(s)://(not turso),ftp://,mssql://,sqlserver://,oracle://,cockroachdb://,mariadb://:memory:default /nullpg://…,PG://…:memory:default /null./data/app,/tmp/…/app,app,C:\data\app,C:/data/app:memory:default /nulljdbc:postgresql://h/db,redis:h(a scheme with no//):memory:default /nullredis://h/x.wasm.db,jdbc:x/x.wasm.db(an unrecognised scheme with a.wasm.dbending)sqlite://x.db,sqlite://x.wasm.db//x.db; sqlite-wasm on//x.wasm.db(the filesystem root)x.db; sqlite-wasm onx.wasm.dbPins
All under
packages/cli/src/**, in the unit tier.utils/storage-driver.test.ts:pg:///PG://map to postgres, and four scheme-less paths map to sqlite;''comes back only for an absent or blank URL;redis://h/app.dbamong them, each checked for the error type,recognized: false,driverType= the scheme, and that the message namesOS_DATABASE_URL, the scheme and the recognised spellings;jdbc:postgresql://localhost:5432/db,redis:cache,redis://h/app.wasm.dbandjdbc:x/app.wasm.dbare refused at inference, naming the scheme;C:\data\app,C:/data/appand./backup:2026.dbstay sqlite paths;sqlite://x.db,sqlite:x.db,sqlite:///var/lib/objectstack/x.dbandSQLITE://x.dbresolve to the filex.dbor/var/lib/objectstack/x.db, with the typed URL kept for the banner; afile:/sqlite:*.wasm.dbselects sqlite-wasm;sqlite://x.wasm.dbis the WASM engine onx.wasm.db.inferDriverTypeFromUrl('redis://localhost') === ''pinned the defect. It is replaced.utils/driver-vocabulary-parity.test.ts, a new block, "the OS_DATABASE_URL door", driving both hosts:displayUrl === url) in dev and in prod. A bare driver-id check would read the dev:memory:default as agreement.jdbc:postgresql://localhost:5432/dbandredis:cache, thenredis://h/app.wasm.dbandjdbc:x/app.wasm.db. The runtime host's answer for these is deliberately not pinned: today it opens them as a file, the gap runtime(standalone host): anOS_DATABASE_URLwith an opaque scheme (jdbc:postgresql://…,redis:h) is opened as a SQLite file path and creates directories, against the module's own "unknown URL schemes throw" #23765 names.sqlite://x.dbandsqlite:x.dbasx.db,sqlite:///var/lib/objectstack/x.dbas itself, andsqlite://x.wasm.dbasx.wasm.dbon the WASM engine.file:./data/app.wasm.dbas sqlite-wasm.Not landed: a real-boot pin. The fixture that spawns the built
bin/run.jsin production posture lives underpackages/cli/test/. A new spawner there also joins the built-entrypoint roster inscripts/check-cli-test-child-env.mjs. Both are outside this card's file surface. The CLI's existing refusals (memory://, unknownOS_DATABASE_DRIVER) are pinned at this same unit layer in both modes. The boots above are the one-off proof of the exit path.Ablation (one per behaviour changed; WRAP mode of
scripts/ablation-replace.mjs, on-disk proof, restore proven as blob == HEAD and an emptygit diff HEAD)return ''restored for an unrecognisedSCHEME://pgdropped from the postgres arm*.db/:memory:arm in place of the scheme-less armredis://h/app.dbamong themRound 1, at
40fe66778: the same tool in WRAP mode over the two pinned files (149 tests, baseline 149 passed). The subject issrc(both files import./storage-driver.js), so nodist/is involved. Each mutation is proven on disk (anchor x1 → x0 and a new blob). Each restore is proven as blob == HEAD893ea05be3edwith an emptygit diff HEAD, by the tool and again by an outer trap.jdbc:postgresql://…,redis:cacheandjdbc:x/app.wasm.dbrows, in both filesC:\data\appandC:/data/appsqlite:in place ofnormalizeDatabaseUrlsqlite://x.db,sqlite:///var/lib/objectstack/x.db,SQLITE://x.db(sqlite:x.dbstays green, since the old strip handled it)sqlite:sqlite://x.wasm.db, in both files*.wasm.dbtest moved back above every scheme checkredis://h/app.wasm.dbandjdbc:x/app.wasm.db, in both filesThe first A1 attempt was a no-op: the tool refused it because the anchor was contained in the replacement, and restored the file. It was re-run with a non-overlapping replacement; the counts above are from that run.
Tests and gates (head
40fe66778)pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2, in two shards (--shard=1/2,--shard=2/2): 148 + 147 = 295 files, 2464 + 1953 = 4417 tests passed. The diff touches no integration-tier file, spawn entry or driver/kernel boot path, so theintegrationlayer is declared to CI.pnpm --filter @objectstack/cli typecheck: exit 0.tsc --noEmit --listFileslists both changed test files (include: ["src"], no exclude), thencheck:test-typecheckran OK.pnpm exec eslint packages/cli --no-inline-config: exit 0, 696 files, 0 errors, 0 warnings.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 64 commands, run in order, each exit captured before any pipe. All 64 exited 0,check:dual-build-cjs-loads(110 require entry points across 68 packages load) andcheck:i18n-coverage(14 configs, none new) included.--ranreconciliation: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The derivation read a tree 19 commits behindorigin/main, with 4 derived-from files moved there. The same derivation over the same 4 paths, in a throwaway tree atorigin/mainec44cf60b, printed the identical 64 commands.Acceptance notes
Round 1: the
*.wasm.dbrule goes past the REWORK's two asks, in ask ①'s class. A.wasm.dbname tested above every scheme check rescuedredis://h/x.wasm.dbandjdbc:x/x.wasm.dbinto a SQLite file. The CLI host now refuses both, while the runtime host still acceptsredis://h/x.wasm.db(measured above). The seat records that host's answer on runtime(standalone host): anOS_DATABASE_URLwith an opaque scheme (jdbc:postgresql://…,redis:h) is opened as a SQLite file path and creates directories, against the module's own "unknown URL schemes throw" #23765.Round 1: the write-failure lines of the
file://probe below carry a[driver-sqlite-wasm]prefix while the banner readsSqlDriver(better-sqlite3). Observed once, not investigated. Carrier: none.serve.ts's step-2 comment block lists the URL-scheme priority. It now omitspg://, the scheme-less arm and the unrecognised-scheme refusal. It is not false, only incomplete.serve.tsis held by cli/serve: with --no-server the ready banner still prints "API: http://localhost:PORT/" and an MCP connect URL, though no HTTP server is composed and nothing listens #23623 and is not edited here. Carrier: none.The no-storage refusal's phrase "no recognized OS_DATABASE_URL" (
serve.ts) is now redundant rather than false: a URL that is set and unrecognised no longer reaches it.content/docs/deployment/cli.mdx:522saysos start"auto-detects the driver from the database URL scheme (… otherwise sqlite …)". The runtime host already refused an unrecognised scheme before this change, and this change does not make the sentence false. No docs sentence is made false here, so no docs edit is made. Carrier: none.Mechanism hypothesis H3 is partly false.
os startover a HOST config does reach this path, through its productionservechild (measured above). Over a plain config,os startgoes through the runtime host, as H3 said.Found while measuring
Round 0's three findings, as the seat dispositioned them: 1 is #23764; 2 is folded into this PR (the alias rule above); 3 is #23765, whose CLI half (the refusal) lands here.
Round 1 found one more, for the seat (not changed here). The CLI host reads
file://NAMEas//NAME, a file at the filesystem root, where the runtime host opens./NAME. Measured withOS_DATABASE_URL=file://proc/os23633-probe.db os serve --devover a host config:✓ Server is ready, banner→ file://proc/os23633-probe.db, and every flush of the database failed with ENOENT on//proc/os23633-probe.db, so it answered from memory only. The runtime host, over a plain config, opened./proc/os23633-probe.db. The cause is thefile:strip in both sqlite arms ofresolveStorageDefinition, unchanged frommain; the runtime host stripsfile:and an optional//.Round 0's text, as written then:
os dev --no-watchover a plain config withOS_DATABASE_URL=redis://admin:PASSWORD@127.0.0.1:6399/0. Its🗄️ Database:row redacts the credentials, then the refusal[StandaloneStack] Unsupported database URL scheme: redis://admin:PASSWORD@…prints them in full. Also exception: security. The landing ispackages/runtime/src/standalone-stack.ts(detectDriverFromUrl's throw). Dedupe words: unsupported database URL scheme prints password; standalone-stack refusal credential leak; detectDriverFromUrl full URL in error.sqlite://x.dbat the filesystem root. class a · reach: measured withOS_DATABASE_URL=sqlite://os23633-probe.db os serve --devover a host config. The banner read→ sqlite://os23633-probe.db, and the database was written to/os23633-probe.db. The runtime host normalises the same URL tofile:and opens./os23633-probe.db. The cause isresolveStorageDefinition's sqlite arm, which stripssqlite:and leaves//x.db. Dedupe words: sqlite:// double slash absolute path; CLI sqlite alias not normalised to file:; resolveStorageDefinition sqlite filename root.//becomes a SQLite file on both hosts. class a · reach: measured withOS_DATABASE_URL=jdbc:postgresql://127.0.0.1:5999/db os serve --devover a plain config (runtime host). It printed✓ Server is ready,→ jdbc:postgresql://127.0.0.1:5999/db, and created./jdbc:postgresql:/127.0.0.1:5999/db. The runtime host's own header says an unknown scheme never falls back to SQLite. Its scheme-less test only asks whetherSCHEME://is present. The CLI host mirrors it by this card's ruling (it never refuses what the runtime host accepts), and the new parity row holds the two together. The fix is on both hosts at once. Dedupe words: jdbc URL treated as sqlite file; opaque scheme bare path arm; detectDriverFromUrl bogus directory.Generated by Claude Code