From 3e4f1f0ebf0d37347fe38bcdb21fb7a07c24cde0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 12:34:19 +0000 Subject: [PATCH 1/5] fix(security): stop leaking credentials and tighten on-disk permissions - config set: mask api_key/access_token/access_key_id/access_key_secret in the confirmation echo. It previously printed the stored secret verbatim to stdout (CI logs, pipes, screen shares), unlike `config show` / `auth status` which already maskToken(). - http / knowledge retrieve: use maskToken() in --verbose request logs instead of printing the first 8 chars of the bearer token / AccessKey id. - telemetry: write telemetry.jsonl with mode 0600 (was created world-readable by default), matching the other credential-area writers. - ensureConfigDir: chmod 0700 after mkdir, so a pre-existing ~/.bailian created by an older build/another tool (where mkdir's mode is ignored) holding cleartext credentials gets locked down too. Best-effort; never fatal. https://claude.ai/code/session_017ZGQCjwNQF5Pz96gLUnnG1 --- packages/cli/src/commands/config/set.ts | 11 ++++++++++- packages/cli/src/commands/knowledge/retrieve.ts | 3 ++- packages/core/src/client/http.ts | 3 ++- packages/core/src/config/paths.ts | 9 +++++++++ packages/core/src/telemetry/sink.ts | 2 +- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/config/set.ts b/packages/cli/src/commands/config/set.ts index 004be0d..9a0288e 100644 --- a/packages/cli/src/commands/config/set.ts +++ b/packages/cli/src/commands/config/set.ts @@ -1,6 +1,7 @@ import { defineCommand, detectOutputFormat, + maskToken, readConfigFile, writeConfigFile, BailianError, @@ -28,6 +29,11 @@ const VALID_KEYS = [ "workspace_id", ]; +// Keys whose values are secrets. Their stored value must never be echoed back in +// cleartext (CI logs, pipes, shared terminals); show a masked form instead — the +// same policy `config show` and `auth status` already follow. +const SECRET_KEYS = new Set(["api_key", "access_token", "access_key_id", "access_key_secret"]); + // Allow hyphen-style keys (e.g. default-text-model → default_text_model) const KEY_ALIASES: Record = { "base-url": "base_url", @@ -120,7 +126,10 @@ export default defineCommand({ await writeConfigFile(existing); if (!config.quiet) { - emitResult({ [resolvedKey]: existing[resolvedKey] }, format); + const shown = SECRET_KEYS.has(resolvedKey) + ? maskToken(String(existing[resolvedKey])) + : existing[resolvedKey]; + emitResult({ [resolvedKey]: shown }, format); } }, }); diff --git a/packages/cli/src/commands/knowledge/retrieve.ts b/packages/cli/src/commands/knowledge/retrieve.ts index 349fb64..37dd073 100644 --- a/packages/cli/src/commands/knowledge/retrieve.ts +++ b/packages/cli/src/commands/knowledge/retrieve.ts @@ -2,6 +2,7 @@ import { defineCommand, signRequest, detectOutputFormat, + maskToken, type Config, type GlobalFlags, type KnowledgeRetrieveRequest, @@ -105,7 +106,7 @@ export default defineCommand({ if (config.verbose) { process.stderr.write(`> POST ${url}\n`); - process.stderr.write(`> AK: ${accessKeyId.slice(0, 8)}...\n`); + process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`); } const timeoutMs = config.timeout * 1000; diff --git a/packages/core/src/client/http.ts b/packages/core/src/client/http.ts index 1a30f30..22490b3 100644 --- a/packages/core/src/client/http.ts +++ b/packages/core/src/client/http.ts @@ -4,6 +4,7 @@ import { BailianError } from "../errors/base.ts"; import { ExitCode } from "../errors/codes.ts"; import { resolveCredential } from "../auth/resolver.ts"; import { mapApiError } from "../errors/api.ts"; +import { maskToken } from "../utils/token.ts"; import { SOURCE_CONFIG, trackingHeaders } from "./headers.ts"; export interface RequestOpts { @@ -58,7 +59,7 @@ export async function request(config: Config, opts: RequestOpts): Promise ${opts.method ?? "GET"} ${opts.url}`); - console.error(`> Auth: ${credential.token.slice(0, 8)}...`); + console.error(`> Auth: ${maskToken(credential.token)}`); console.error(`> x-dashscope-source-config: ${SOURCE_CONFIG}`); } } diff --git a/packages/core/src/config/paths.ts b/packages/core/src/config/paths.ts index 9e0de01..78e3f45 100644 --- a/packages/core/src/config/paths.ts +++ b/packages/core/src/config/paths.ts @@ -20,4 +20,13 @@ export async function ensureConfigDir(): Promise { const dir = getConfigDir(); const fs = await import("fs/promises"); await fs.mkdir(dir, { recursive: true, mode: 0o700 }); + // `mkdir`'s `mode` only applies to directories it creates (and is masked by + // umask). A config dir created by an older build or another tool may still be + // world/group-readable while holding cleartext credentials, so tighten it + // explicitly. Best-effort: never let a chmod failure break the command. + try { + await fs.chmod(dir, 0o700); + } catch { + /* best effort */ + } } diff --git a/packages/core/src/telemetry/sink.ts b/packages/core/src/telemetry/sink.ts index d7ecc16..7ecf529 100644 --- a/packages/core/src/telemetry/sink.ts +++ b/packages/core/src/telemetry/sink.ts @@ -90,7 +90,7 @@ export async function localSink(event: TrackingEvent): Promise { // 文件还不存在,忽略 } - appendFileSync(path, JSON.stringify(event) + "\n"); + appendFileSync(path, JSON.stringify(event) + "\n", { mode: 0o600 }); } catch { // 埋点逻辑任何异常都不能影响 CLI 主流程 } From d24f203d68ba620abe8895850dd82923f257297f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 12:34:22 +0000 Subject: [PATCH 2/5] fix(security): encode URL path segments and bound SSE buffering - endpoints: encodeURIComponent the id segments (task_id, app_id, node_id, schema_id) interpolated into request URLs. task_id in particular comes from the server's async-submit response and is fetched back with the bearer token attached, so an unencoded value could steer the authenticated follow-up request to a different path on the host. - stream (SSE parser): cap the in-memory buffer (16 MiB). A stream that never emits a newline, or that builds one enormous event from many data: lines, could otherwise grow the buffer without bound and exhaust process memory. https://claude.ai/code/session_017ZGQCjwNQF5Pz96gLUnnG1 --- packages/core/src/client/endpoints.ts | 8 ++++---- packages/core/src/client/stream.ts | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/core/src/client/endpoints.ts b/packages/core/src/client/endpoints.ts index fec9f0b..2c8e0df 100644 --- a/packages/core/src/client/endpoints.ts +++ b/packages/core/src/client/endpoints.ts @@ -24,13 +24,13 @@ export function videoGenerateEndpoint(baseUrl: string): string { // ---- Async Task Query ---- export function taskEndpoint(baseUrl: string, taskId: string): string { - return `${baseUrl}/api/v1/tasks/${taskId}`; + return `${baseUrl}/api/v1/tasks/${encodeURIComponent(taskId)}`; } // ---- Application (Agent / Workflow) ---- export function appCompletionEndpoint(baseUrl: string, appId: string): string { - return `${baseUrl}/api/v1/apps/${appId}/completion`; + return `${baseUrl}/api/v1/apps/${encodeURIComponent(appId)}/completion`; } // ---- Memory (DashScope v2) ---- @@ -48,7 +48,7 @@ export function memoryListEndpoint(baseUrl: string): string { } export function memoryNodeEndpoint(baseUrl: string, nodeId: string): string { - return `${baseUrl}/api/v2/apps/memory/memory_nodes/${nodeId}`; + return `${baseUrl}/api/v2/apps/memory/memory_nodes/${encodeURIComponent(nodeId)}`; } // ---- Speech Synthesis (TTS) ---- @@ -70,7 +70,7 @@ export function profileSchemaEndpoint(baseUrl: string): string { } export function userProfileEndpoint(baseUrl: string, schemaId: string): string { - return `${baseUrl}/api/v2/apps/memory/profile_schemas/${schemaId}/profiles`; + return `${baseUrl}/api/v2/apps/memory/profile_schemas/${encodeURIComponent(schemaId)}/profiles`; } // ---- MCP Services (Streamable HTTP) ---- diff --git a/packages/core/src/client/stream.ts b/packages/core/src/client/stream.ts index 56904aa..6fe6ac2 100644 --- a/packages/core/src/client/stream.ts +++ b/packages/core/src/client/stream.ts @@ -1,3 +1,6 @@ +import { BailianError } from "../errors/base.ts"; +import { ExitCode } from "../errors/codes.ts"; + export interface ServerSentEvent { event?: string; data: string; @@ -11,12 +14,20 @@ export async function* parseSSE(response: Response): AsyncGenerator MAX_SSE_BUFFER) { + throw new BailianError("SSE stream exceeded the maximum buffer size.", ExitCode.GENERAL); + } const lines = buffer.split("\n"); buffer = lines.pop() || ""; @@ -43,6 +54,12 @@ export async function* parseSSE(response: Response): AsyncGenerator MAX_SSE_BUFFER) { + throw new BailianError( + "SSE event exceeded the maximum buffer size.", + ExitCode.GENERAL, + ); + } break; case "event": event.event = value; From 8b9986bb4740844d984c1af5e1915ce85e34ad9f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 12:34:24 +0000 Subject: [PATCH 3/5] fix(security): harden pipeline planning, pointer traversal, and concurrency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - expressions: never execute $js during planning/dry-run. `pipeline run --dry-run` is the command a cautious user runs to preview an unfamiliar pipeline; it must not run embedded JavaScript. Planning now returns the expression placeholder instead of calling new Function. - schema (getByJsonPointer): block __proto__/constructor/prototype and require own properties, so a crafted $from/$input path cannot pull object internals (e.g. constructor) out of step output and feed them downstream. - scheduler: clamp --concurrency to a maximum (64) to bound fan-out so a single run cannot launch an unbounded number of concurrent API calls / downloads. Note: the runtime new Function sinks in script/js and $js (arbitrary host code execution) are intentionally left unchanged here — remediating them is a design decision (sandbox vs. literal-only code) for the maintainers; see PR notes. https://claude.ai/code/session_017ZGQCjwNQF5Pz96gLUnnG1 --- packages/cli/src/pipeline/expressions.ts | 23 ++++------------------- packages/cli/src/pipeline/scheduler.ts | 6 +++++- packages/cli/src/pipeline/schema.ts | 8 ++++++++ 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/cli/src/pipeline/expressions.ts b/packages/cli/src/pipeline/expressions.ts index ef11cf1..4e313af 100644 --- a/packages/cli/src/pipeline/expressions.ts +++ b/packages/cli/src/pipeline/expressions.ts @@ -289,25 +289,10 @@ function resolvePlannedExpression( return combineResolved(undefined, undefined, false, false); } if ("$js" in expression) { - const argsExpressions = (expression.args ?? {}) as Record; - const hasFrom = Object.values(argsExpressions).some((v) => isRecord(v) && "$from" in v); - if (hasFrom) return combineResolved({ ...expression }, { ...expression }, false); - const code = expression.$js as string; - const resolvedArgs: Record = {}; - let sensitive = false; - for (const [key, argExpr] of Object.entries(argsExpressions)) { - const resolved = resolvePlannedExpression(argExpr, pipeline, runtimeInput); - resolvedArgs[key] = resolved.value; - sensitive = sensitive || resolved.sensitive; - } - try { - // eslint-disable-next-line @typescript-eslint/no-implied-eval - const fn = new Function("args", `return (${code})`); - const value = fn(resolvedArgs); - return combineResolved(value, sensitive ? REDACTED : value, sensitive); - } catch { - return combineResolved({ ...expression }, { ...expression }, false); - } + // Planning / dry-run must be a non-executing preview: never run user + // JavaScript here. Surface the expression as an unresolved placeholder so a + // `--dry-run` of an untrusted pipeline cannot trigger code execution. + return combineResolved({ ...expression }, { ...expression }, false); } return combineResolved(expression, expression, false); } diff --git a/packages/cli/src/pipeline/scheduler.ts b/packages/cli/src/pipeline/scheduler.ts index 8a7ab7a..5a2458c 100644 --- a/packages/cli/src/pipeline/scheduler.ts +++ b/packages/cli/src/pipeline/scheduler.ts @@ -70,6 +70,8 @@ export function orderReports( return [...reports].sort((a, b) => (index.get(a.id) ?? 0) - (index.get(b.id) ?? 0)); } +const MAX_CONCURRENCY = 64; + export function normalizeConcurrency(value: number | undefined): number { if (value === undefined) return 1; if (!Number.isInteger(value) || value < 1) { @@ -77,5 +79,7 @@ export function normalizeConcurrency(value: number | undefined): number { details: { issues: ["concurrency must be a positive integer"] }, }); } - return value; + // Cap fan-out so a single run cannot launch an unbounded number of concurrent + // API calls / downloads and exhaust sockets, file descriptors, or memory. + return Math.min(value, MAX_CONCURRENCY); } diff --git a/packages/cli/src/pipeline/schema.ts b/packages/cli/src/pipeline/schema.ts index 744d826..6ad4b2b 100644 --- a/packages/cli/src/pipeline/schema.ts +++ b/packages/cli/src/pipeline/schema.ts @@ -91,6 +91,14 @@ export function getByJsonPointer(value: unknown, pointer: string): unknown { continue; } if (isRecord(current)) { + // A JSON pointer over data must not reach object internals. Block + // prototype-polluting keys and only follow own properties so a crafted + // `$from`/`$input` path cannot pull out `constructor`/`__proto__` and feed + // it into downstream consumers. + if (segment === "__proto__" || segment === "constructor" || segment === "prototype") { + return undefined; + } + if (!Object.prototype.hasOwnProperty.call(current, segment)) return undefined; current = current[segment]; continue; } From bb9f941849cd8cf0eb8b9ce76b235c32ba109ec5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 12:44:48 +0000 Subject: [PATCH 4/5] fix(security): require script/js `code` to be a literal (block untrusted-code RCE) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit script/js executes its `code` as host JavaScript (via new Function), and a step's `code` is a *resolved* input — so it could be written as `{ $from: }`, turning model/API output into the body of the executed function (untrusted data -> arbitrary host code execution). Pipeline validation now requires script/js `code` to be a literal string: any $from/expression-sourced code is rejected. Authoring a literal script/js step remains supported (the pipeline file is the trust boundary, like a shell/npm script). Combined with "dry-run never executes $js", this closes the path where untrusted text reaches the JS sink. Adds regression tests: $from-sourced code rejected, literal code accepted, dry-run does not execute $js, getByJsonPointer blocks prototype/inherited keys, and concurrency clamps to the maximum. https://claude.ai/code/session_017ZGQCjwNQF5Pz96gLUnnG1 --- packages/cli/src/pipeline/validation.ts | 15 +++++ packages/cli/tests/index.test.ts | 73 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/packages/cli/src/pipeline/validation.ts b/packages/cli/src/pipeline/validation.ts index 87512d2..642dfc4 100644 --- a/packages/cli/src/pipeline/validation.ts +++ b/packages/cli/src/pipeline/validation.ts @@ -125,6 +125,21 @@ function collectPipelineSemanticIssues( `semantic: step "${stepLabel}" timeout must be a positive number of seconds or duration string`, ); } + + // `script/js` executes its `code` as host JavaScript. Require it to be a + // literal string in the pipeline definition: code sourced from another step + // ($from) or any expression is rejected, so untrusted/model-generated text + // can never become the body of the executed function. + if (step.type === "script/js") { + const code = isRecord(step.input) ? step.input.code : undefined; + if (typeof code !== "string") { + issues.push( + `semantic: step "${stepLabel}" (script/js) requires a literal string "code"; ` + + `code sourced from another step ($from) or an expression is not allowed, ` + + `since it would execute untrusted text as host code`, + ); + } + } } // Check dependency references diff --git a/packages/cli/tests/index.test.ts b/packages/cli/tests/index.test.ts index 507a100..f6853c0 100644 --- a/packages/cli/tests/index.test.ts +++ b/packages/cli/tests/index.test.ts @@ -1,6 +1,9 @@ import { expect, test } from "vite-plus/test"; import { createStepDispatcher } from "../src/pipeline/dispatcher.ts"; import { executePipeline } from "../src/pipeline/executor.ts"; +import { collectPipelineIssues } from "../src/pipeline/validation.ts"; +import { getByJsonPointer } from "../src/pipeline/schema.ts"; +import { normalizeConcurrency } from "../src/pipeline/scheduler.ts"; import { WORKFLOW_VERSION, type PipelineDefinition } from "../src/pipeline/types.ts"; test("cli package skeleton", () => { @@ -34,3 +37,73 @@ test("pipeline execution can use an isolated step dispatcher", async () => { hasSignal: true, }); }); + +test("dry-run never executes $js expressions (preview must not run code)", async () => { + const dispatcher = createStepDispatcher(); + dispatcher.registerStep("test/echo", (input) => ({ data: input })); + const flag = "__bailian_dryrun_should_not_run__"; + delete (globalThis as Record)[flag]; + + const pipeline: PipelineDefinition = { + version: WORKFLOW_VERSION, + steps: [ + { + id: "s1", + type: "test/echo", + input: { probe: { $js: `(globalThis[${JSON.stringify(flag)}] = true), 1` } }, + }, + ], + }; + + const report = await executePipeline(pipeline, {}, { stepDispatcher: dispatcher, dryRun: true }); + expect(report.status).toBe("planned"); + expect((globalThis as Record)[flag]).toBeUndefined(); +}); + +test("script/js rejects non-literal code sourced from another step ($from)", () => { + const dispatcher = createStepDispatcher(); + dispatcher.registerStep("test/echo", (input) => ({ data: input })); + dispatcher.registerStep("script/js", () => ({ data: {} })); + + const pipeline: PipelineDefinition = { + version: WORKFLOW_VERSION, + steps: [ + { id: "gen", type: "test/echo", input: { message: "x" } }, + { + id: "run", + type: "script/js", + input: { code: { $from: "gen", path: "/data/message" } as never }, + }, + ], + }; + + const issues = collectPipelineIssues(pipeline, dispatcher); + expect(issues.some((issue) => issue.includes('literal string "code"'))).toBe(true); +}); + +test("script/js accepts a literal string code", () => { + const dispatcher = createStepDispatcher(); + dispatcher.registerStep("script/js", () => ({ data: {} })); + + const pipeline: PipelineDefinition = { + version: WORKFLOW_VERSION, + steps: [{ id: "run", type: "script/js", input: { code: "return 1" } }], + }; + + expect(collectPipelineIssues(pipeline, dispatcher)).toEqual([]); +}); + +test("getByJsonPointer refuses prototype keys and inherited properties", () => { + const obj = { a: { b: 1 } }; + expect(getByJsonPointer(obj, "/a/b")).toBe(1); + expect(getByJsonPointer(obj, "/__proto__")).toBeUndefined(); + expect(getByJsonPointer(obj, "/constructor")).toBeUndefined(); + expect(getByJsonPointer(obj, "/a/constructor/constructor")).toBeUndefined(); + expect(getByJsonPointer(obj, "/toString")).toBeUndefined(); +}); + +test("normalizeConcurrency clamps to a safe maximum", () => { + expect(normalizeConcurrency(undefined)).toBe(1); + expect(normalizeConcurrency(4)).toBe(4); + expect(normalizeConcurrency(100000)).toBe(64); +}); From ba074f566d7df0483ada2daa21b920c3be4a7ee0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 12:44:50 +0000 Subject: [PATCH 5/5] fix(security): validate base_url / console_gateway_url as real http(s) URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The config file accepted any value that merely starts with "http" (so even "httpfoo://evil" passed) for base_url and console_gateway_url — origins the client sends the Bearer token to. Validate them with `new URL()` and an http:/https: protocol check instead, rejecting malformed values. Valid http(s) URLs (including custom proxies and local http) are unaffected. https://claude.ai/code/session_017ZGQCjwNQF5Pz96gLUnnG1 --- packages/core/src/config/schema.ts | 20 +++++++++++++++++--- packages/core/tests/index.test.ts | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/core/src/config/schema.ts b/packages/core/src/config/schema.ts index c568a2e..7fad89f 100644 --- a/packages/core/src/config/schema.ts +++ b/packages/core/src/config/schema.ts @@ -38,6 +38,21 @@ export interface ConfigFile { const VALID_REGIONS = new Set(["cn", "us", "intl"]); const VALID_OUTPUTS = new Set(["text", "json"]); +/** + * A syntactically valid absolute http(s) URL. Used to validate `base_url` and + * `console_gateway_url` from the config file: the credential-bearing client + * sends the Bearer token to these origins, so a bare `startsWith("http")` check + * (which also accepts e.g. "httpfoo://…") is too loose. + */ +function isHttpUrl(value: string): boolean { + try { + const u = new URL(value); + return u.protocol === "http:" || u.protocol === "https:"; + } catch { + return false; + } +} + export function parseConfigFile(raw: unknown): ConfigFile { if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}; const obj = raw as Record; @@ -50,8 +65,7 @@ export function parseConfigFile(raw: unknown): ConfigFile { out.access_token = obj.accessToken; if (typeof obj.region === "string" && VALID_REGIONS.has(obj.region)) out.region = obj.region as Region; - if (typeof obj.base_url === "string" && obj.base_url.startsWith("http")) - out.base_url = obj.base_url; + if (typeof obj.base_url === "string" && isHttpUrl(obj.base_url)) out.base_url = obj.base_url; if (typeof obj.output === "string" && VALID_OUTPUTS.has(obj.output)) out.output = obj.output as ConfigFile["output"]; if (typeof obj.output_dir === "string" && obj.output_dir.length > 0) @@ -73,7 +87,7 @@ export function parseConfigFile(raw: unknown): ConfigFile { out.access_key_secret = obj.access_key_secret; if (typeof obj.workspace_id === "string" && obj.workspace_id.length > 0) out.workspace_id = obj.workspace_id; - if (typeof obj.console_gateway_url === "string" && obj.console_gateway_url.startsWith("http")) + if (typeof obj.console_gateway_url === "string" && isHttpUrl(obj.console_gateway_url)) out.console_gateway_url = obj.console_gateway_url; if (typeof obj.telemetry === "boolean") out.telemetry = obj.telemetry; diff --git a/packages/core/tests/index.test.ts b/packages/core/tests/index.test.ts index a81d1d0..613a529 100644 --- a/packages/core/tests/index.test.ts +++ b/packages/core/tests/index.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "vite-plus/test"; import type { Config } from "../src/index.ts"; import { BailianError, ExitCode, McpClient, mapApiError, request } from "../src/index.ts"; +import { parseConfigFile } from "../src/config/schema.ts"; function testConfig(overrides: Partial = {}): Config { return { @@ -173,3 +174,16 @@ test("McpClient uses injected client identity for initialize and User-Agent", as params: { clientInfo: { name: "test-client", version: "9.8.7" } }, }); }); + +test("parseConfigFile accepts only well-formed http(s) base_url / console_gateway_url", () => { + expect(parseConfigFile({ base_url: "https://dashscope.aliyuncs.com" }).base_url).toBe( + "https://dashscope.aliyuncs.com", + ); + expect(parseConfigFile({ base_url: "http://localhost:8080" }).base_url).toBe( + "http://localhost:8080", + ); + // Previously accepted because the value merely "starts with http". + expect(parseConfigFile({ base_url: "httpfoo://evil" }).base_url).toBeUndefined(); + expect(parseConfigFile({ base_url: "not a url" }).base_url).toBeUndefined(); + expect(parseConfigFile({ console_gateway_url: "ftp://x" }).console_gateway_url).toBeUndefined(); +});