Keep a secret setting's raw value out of the boot error that rejects it
CI / full-gate (push) Successful in 3m23s

This commit is contained in:
2026-10-31 13:38:27 +01:00
parent 622059101f
commit 8f7bc59b38
5 changed files with 50 additions and 13 deletions
+20
View File
@@ -115,6 +115,26 @@ test("each type coerces from the environment, and a bad value fails loud", () =>
assert.match(bad.errors.join("\n"), /PLUGIN_SETTING_P_BASE.*URL/);
});
test("a secret's bad value never reaches the boot error, whatever its type", () => {
const cases: [SettingDecl, string, RegExp][] = [
[{ key: "s", secret: true, type: "boolean" }, "hunter2-bool", /PLUGIN_SETTING_P_S.*"true".*"false"/],
[{ key: "s", secret: true, type: "enum", values: ["a", "b"] }, "hunter2-enum", /PLUGIN_SETTING_P_S.*a, b/],
[{ key: "s", secret: true, type: "number" }, "hunter2-number", /PLUGIN_SETTING_P_S.*number/],
[{ key: "s", secret: true, type: "url" }, "postgres user:hunter2-url@db", /PLUGIN_SETTING_P_S.*URL/],
];
for (const [decl, raw, names] of cases) {
const errors = resolveSettings([pluginWith("p", [decl])], { PLUGIN_SETTING_P_S: raw }).errors;
assert.equal(errors.length, 1, decl.type);
assert.match(errors[0] ?? "", names, decl.type);
assert.ok(!(errors[0] ?? "").includes("hunter2"), decl.type);
}
});
test("a non-secret's bad value is quoted in the boot error, so the operator sees the typo", () => {
const errors = resolveSettings([pluginWith("p", [{ key: "s", type: "url" }])], { PLUGIN_SETTING_P_S: "not a url" }).errors;
assert.match(errors[0] ?? "", /got "not a url"/);
});
test("a boolean is only \"true\"/\"false\" — a typo never degrades to false", () => {
const plugins = [pluginWith("p", [{ default: true, key: "flag", type: "boolean" }])];
assert.equal(resolveSettings(plugins, { PLUGIN_SETTING_P_FLAG: "false" }).values.get("p")?.["flag"], false);
+7 -5
View File
@@ -231,26 +231,28 @@ function summarize(decl: SettingDecl, name: string, source: SettingSummary["sour
};
}
// A coerced value, or the boot error naming the variable and what it accepts.
// A coerced value, or the boot error naming the variable and what it accepts. A secret's value
// stays out of it: the error reaches the log.
function coerce(decl: SettingDecl, raw: string, name: string): { value: SettingValue } | string {
const got = decl.secret === true ? "" : `, got "${raw}"`;
switch (decl.type) {
case "boolean":
if (raw === "true") return { value: true };
if (raw === "false") return { value: false };
return `${name} must be "true" or "false", got "${raw}"`;
return `${name} must be "true" or "false"${got}`;
case "enum":
return (decl.values ?? []).includes(raw)
? { value: raw }
: `${name} must be one of ${(decl.values ?? []).join(", ")}, got "${raw}"`;
: `${name} must be one of ${(decl.values ?? []).join(", ")}${got}`;
case "number": {
const value = Number(raw);
return Number.isFinite(value) ? { value } : `${name} must be a number, got "${raw}"`;
return Number.isFinite(value) ? { value } : `${name} must be a number${got}`;
}
case "url":
try {
new URL(raw);
} catch {
return `${name} is not a valid URL: ${raw}`;
return `${name} must be a valid URL${got}`;
}
return { value: raw };
case "string":