From 66e290700f6dfe981d5e206e92df013a8de23e27 Mon Sep 17 00:00:00 2001 From: Mohd Salauddin Date: Mon, 3 Aug 2026 21:08:59 +0530 Subject: [PATCH] Reject unknown top-level qm.config.jsonc fields --- cli/src/config.ts | 32 ++++++++++++++++++++++++++++++++ cli/test/config.test.ts | 6 ++++++ 2 files changed, 38 insertions(+) diff --git a/cli/src/config.ts b/cli/src/config.ts index 2277e65c..f6da0e47 100644 --- a/cli/src/config.ts +++ b/cli/src/config.ts @@ -478,10 +478,42 @@ export function readConfigOrgId(path: string): string | undefined { } } +const VALID_TOP_LEVEL_KEYS: ReadonlySet = new Set([ + "contract", + "orgId", + "publicUrl", + "apiUrl", + "target", + "model", + "modelProvider", + "basePort", + "services", + "plugins", + "skills", + "env", + "secretEnv", + "securityScreen", + "vms", + "imageOverrides", + "sandbox", + "appPrefix", + "region", + "flyOrg", + "imageFrom", + "deployAppPrefix", + "aws", +]); + function validate(raw: unknown, path: string): QmConfig { if (!isPlainObject(raw)) throw new CliError(`${path}: expected a JSON object`); const o = raw; + for (const key of Object.keys(o)) { + if (!VALID_TOP_LEVEL_KEYS.has(key)) { + throw new CliError(`${path}: unknown top-level field ${JSON.stringify(key)}`); + } + } + const contract = o["contract"]; if (contract !== CONTRACT_VERSION) { if (typeof contract === "number" && Number.isInteger(contract)) { diff --git a/cli/test/config.test.ts b/cli/test/config.test.ts index 7d62298d..acbb01c1 100644 --- a/cli/test/config.test.ts +++ b/cli/test/config.test.ts @@ -62,6 +62,12 @@ test("required fields: orgId, target, services (must include core), valid servic const { config } = loadConfigAt(path); assert.deepEqual(config.services, ["core", "web-ui", "admin", "portal"]); }); + withConfig({ unknownField: "bad" }, ({ path }) => + assert.throws(() => loadConfigAt(path), /unknown top-level field "unknownField"/), + ); + withConfig({ org_id: "acme" }, ({ path }) => + assert.throws(() => loadConfigAt(path), /unknown top-level field "org_id"/), + ); }); test("apiUrl must be an http(s) origin URL; the trailing slash is stripped", () => {