From 3ddd364add666699edef8eb92110c4c84046abb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 06:59:29 +0000 Subject: [PATCH] fix(cli): the memory:// / mingo:// refusal names the scheme, never the URL inferDriverTypeFromUrl's retired in-memory engine arm built its refusal from the whole OS_DATABASE_URL, so a user, password or token in a memory:// or mingo:// URL was printed when `os serve` exited. The arm now names the scheme in the runtime host's words ("An OS_DATABASE_URL with the "memory://" scheme"), and carries the scheme, not the URL, as driverType. Pins: a credentialed memory:// and mingo:// URL is refused naming the scheme and the SQLite replacements and none of the placeholders; the same tail under every other scheme classifies as before; both hosts refuse it in the same words. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude --- .changeset/23792-memory-url-refusal-no-url.md | 18 +++++ .../utils/driver-vocabulary-parity.test.ts | 22 +++++++ packages/cli/src/utils/storage-driver.test.ts | 65 +++++++++++++++++-- packages/cli/src/utils/storage-driver.ts | 16 ++++- 4 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 .changeset/23792-memory-url-refusal-no-url.md diff --git a/.changeset/23792-memory-url-refusal-no-url.md b/.changeset/23792-memory-url-refusal-no-url.md new file mode 100644 index 00000000000..f7e77e4dce4 --- /dev/null +++ b/.changeset/23792-memory-url-refusal-no-url.md @@ -0,0 +1,18 @@ +--- +'@objectstack/cli': patch +--- + +fix(cli): `os serve`'s refusal of a `memory://` or `mingo://` `OS_DATABASE_URL` names the URL's scheme and never prints the URL, so a password or token in it no longer reaches the console + +Clause-②: no + +This is the `os serve` path a host config takes (one whose `plugins` hold plugin instances), and the one `OS_MODE=off|none|legacy` takes. `os dev` and `os start` reach it through their `serve` child. Both schemes name the retired in-memory (mingo) engine, and both accept anything after the `//`, a `user:password@` and a query string included. The refusal interpolated the whole URL, and the CLI prints it as it exits 1. + +With `OS_DATABASE_URL=memory://PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:6399/0?authToken=PLACEHOLDER-TOKEN`: + +- FROM: `✗ OS_DATABASE_URL "memory://PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:6399/0?authToken=PLACEHOLDER-TOKEN" selects the in-memory (mingo) engine, …` +- TO: `✗ An OS_DATABASE_URL with the "memory://" scheme selects the in-memory (mingo) engine, …` + +`mingo://` reads the same way, with `"mingo://"`. These are the runtime host's words for the same URL (`os dev` / `os start` over a plain config, and `os migrate`), so the two hosts print one sentence. The rest of the sentence, which names the SQLite replacements (`os dev --fresh`, and the URL `:memory:`), is unchanged. + +What does not change: both schemes are still refused, with the same error class and exit code, in dev and production; every other URL resolves as before, to the same driver. diff --git a/packages/cli/src/utils/driver-vocabulary-parity.test.ts b/packages/cli/src/utils/driver-vocabulary-parity.test.ts index 0b6a7f4889c..571d779783c 100644 --- a/packages/cli/src/utils/driver-vocabulary-parity.test.ts +++ b/packages/cli/src/utils/driver-vocabulary-parity.test.ts @@ -257,6 +257,28 @@ describe('driver vocabulary parity: `os start` and `os migrate` answer alike (#6 }, ); + // The URL door's refusal is printed on the way out, and both schemes take a + // `user:password@` and an `?authToken=`. Both hosts name the SCHEME, never the + // URL, in the same words: the CLI host's refusal is the runtime host's minus + // its `[StandaloneStack]` tag. Placeholder credentials only. + it.each(['memory://', 'mingo://'])( + 'both hosts refuse a credentialed %s OS_DATABASE_URL in the same words, and neither prints the credentials', + (scheme) => { + const url = `${scheme}PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:6399/0?authToken=PLACEHOLDER-TOKEN`; + const cli = refusalMessage(() => resolveDriverType(undefined, url)); + process.env.OS_DATABASE_URL = url; + const standalone = refusalMessage(() => resolveStandaloneDatabase({ artifactPath: '/nonexistent/objectstack.json' })); + for (const [host, message] of [['os start', cli], ['os migrate', standalone]] as const) { + expect(message, `${host} accepted ${scheme}…`).not.toBeNull(); + expect(message, host).toContain(`"${scheme}"`); + for (const placeholder of ['PLACEHOLDER-USER', 'PLACEHOLDER-SECRET', 'PLACEHOLDER-TOKEN']) { + expect(message, `${host} prints ${placeholder}`).not.toContain(placeholder); + } + } + expect(cli).toBe(standalone!.replace(/^\[StandaloneStack\] /, '')); + }, + ); + // The control on the URL door: the replacement the refusals name is accepted // by both hosts, as SQLite — so the refusal is scoped to the engine, not to // the word "memory". diff --git a/packages/cli/src/utils/storage-driver.test.ts b/packages/cli/src/utils/storage-driver.test.ts index 43d71b8bf7f..d08f4db7b65 100644 --- a/packages/cli/src/utils/storage-driver.test.ts +++ b/packages/cli/src/utils/storage-driver.test.ts @@ -61,10 +61,15 @@ describe('inferDriverTypeFromUrl', () => { // The in-memory (mingo) engine's schemes are REFUSED at inference: the engine // is no longer a boot store. Asserted on the error's TYPE and on what it names - // (the URL and the two SQLite replacements), never on its prose. - it.each(['memory://', 'memory://ignored-host', 'mingo://', 'MINGO://x'])( - 'refuses %s at inference, naming the SQLite replacements', - (url) => { + // (the variable, the scheme and the two SQLite replacements), never on its prose. + it.each([ + ['memory://', 'memory://'], + ['memory://ignored-host', 'memory://'], + ['mingo://', 'mingo://'], + ['MINGO://x', 'MINGO://'], + ])( + 'refuses %s at inference, naming its scheme and the SQLite replacements', + (url, scheme) => { let err: unknown; try { inferDriverTypeFromUrl(url); @@ -75,12 +80,62 @@ describe('inferDriverTypeFromUrl', () => { const refusal = err as UnsupportedDriverError; // Not a kind this resolver produces — the allowlist pin's oracle reads this. expect(refusal.recognized).toBe(false); - expect(refusal.message).toContain(`"${url}"`); + expect(refusal.driverType).toBe(scheme); + expect(refusal.message).toContain('OS_DATABASE_URL'); + expect(refusal.message).toContain(`"${scheme}"`); expect(refusal.message).toContain('--fresh'); expect(refusal.message).toContain(':memory:'); }, ); + // The refusal is printed to the console on the way out, and both schemes take + // anything after the `//`: a `user:password@` and an `?authToken=` ride along. + // It names the scheme, never the URL — in the message AND in the field a + // caller may log. Placeholder credentials only. + const PLACEHOLDERS = ['PLACEHOLDER-USER', 'PLACEHOLDER-SECRET', 'PLACEHOLDER-TOKEN'] as const; + const CREDENTIALED_TAIL = 'PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:6399/0?authToken=PLACEHOLDER-TOKEN'; + it.each(['memory://', 'mingo://'])( + 'refuses a %s URL carrying a user, password and token, naming the scheme and none of the three', + (scheme) => { + const url = `${scheme}${CREDENTIALED_TAIL}`; + let err: unknown; + try { + inferDriverTypeFromUrl(url); + } catch (e) { + err = e; + } + expect(err, `inferDriverTypeFromUrl accepted ${scheme}…`).toBeInstanceOf(UnsupportedDriverError); + const refusal = err as UnsupportedDriverError; + expect(refusal.recognized).toBe(false); + expect(refusal.driverType).toBe(scheme); + expect(refusal.message).toContain(`"${scheme}"`); + expect(refusal.message).toContain('--fresh'); + expect(refusal.message).toContain(':memory:'); + for (const placeholder of PLACEHOLDERS) { + expect(refusal.message, `the refusal prints ${placeholder}`).not.toContain(placeholder); + expect(refusal.driverType, `driverType carries ${placeholder}`).not.toContain(placeholder); + } + expect(refusal.message).not.toContain('127.0.0.1'); + }, + ); + + // CONTROL: the same credentialed tail under every other scheme classifies as + // before — the redaction is scoped to the retired engine's arm, not to URLs + // that carry credentials. + it.each([ + ['postgres://', 'postgres'], + ['postgresql://', 'postgres'], + ['pg://', 'postgres'], + ['mysql://', 'mysql'], + ['mysql2://', 'mysql'], + ['mongodb://', 'mongodb'], + ['mongodb+srv://', 'mongodb'], + ['libsql://', 'turso'], + ['wasm-sqlite://', 'sqlite-wasm'], + ])('classifies a %s URL carrying the same user, password and token as %s', (scheme, kind) => { + expect(inferDriverTypeFromUrl(`${scheme}${CREDENTIALED_TAIL}`)).toBe(kind); + }); + // The sqlite `:memory:` PSEUDO-FILE is SQLite's own in-memory mode — NOT the // mingo engine. It stays `sqlite`, and it is the replacement the refusal names. it('keeps sqlite `:memory:` mapped to sqlite (the replacement, distinct from memory://)', () => { diff --git a/packages/cli/src/utils/storage-driver.ts b/packages/cli/src/utils/storage-driver.ts index 893ea05be3e..74287da3831 100644 --- a/packages/cli/src/utils/storage-driver.ts +++ b/packages/cli/src/utils/storage-driver.ts @@ -328,10 +328,20 @@ export function inferDriverTypeFromUrl(url: string | undefined): string { // the same URLs. Kept ABOVE the sqlite test so it is not shadowed, and // deliberately distinct from sqlite's `:memory:` pseudo-file below, which is // the replacement this refusal names. - if (/^(memory|mingo):\/\//i.test(u)) { + // + // The refusal names the SCHEME, never the URL: both schemes accept anything + // after the `//`, a `user:password@` and a `?authToken=` included, and this + // sentence is printed to the console on the way out. The subject is the + // runtime host's own (`refuseRetiredMemoryEngine` in + // `packages/runtime/src/standalone-stack.ts`, module-local there, so written + // out here), and `driver-vocabulary-parity.test.ts` holds the two equal. + // The scheme is also what the error carries as `driverType`, as the + // unrecognised-scheme refusal below does. + const retiredScheme = /^(memory|mingo):\/\//i.exec(u)?.[0]; + if (retiredScheme) { throw new UnsupportedDriverError( - u, - retiredMemoryEngineMessage(`OS_DATABASE_URL "${u}"`), + retiredScheme, + retiredMemoryEngineMessage(`An OS_DATABASE_URL with the "${retiredScheme}" scheme`), // Not a kind this resolver can produce — `database-driver-allowlist.pin` // reads `recognized` to tell the two apart. { recognized: false },