mirror of https://github.com/openclaw/openclaw.git
142 lines
5.5 KiB
TypeScript
142 lines
5.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { z } from "zod";
|
|
import { __test__, isSensitiveConfigPath } from "./schema.hints.js";
|
|
import { OpenClawSchema } from "./zod-schema.js";
|
|
import { sensitive } from "./zod-schema.sensitive.js";
|
|
|
|
const { mapSensitivePaths } = __test__;
|
|
|
|
describe("isSensitiveConfigPath", () => {
|
|
it("matches whitelist suffixes case-insensitively", () => {
|
|
const whitelistedPaths = [
|
|
"maxTokens",
|
|
"maxOutputTokens",
|
|
"maxInputTokens",
|
|
"maxCompletionTokens",
|
|
"contextTokens",
|
|
"totalTokens",
|
|
"tokenCount",
|
|
"tokenLimit",
|
|
"tokenBudget",
|
|
"channels.irc.nickserv.passwordFile",
|
|
];
|
|
for (const path of whitelistedPaths) {
|
|
expect(isSensitiveConfigPath(path)).toBe(false);
|
|
expect(isSensitiveConfigPath(path.toUpperCase())).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("keeps true sensitive keys redacted", () => {
|
|
expect(isSensitiveConfigPath("channels.slack.token")).toBe(true);
|
|
expect(isSensitiveConfigPath("models.providers.openai.apiKey")).toBe(true);
|
|
expect(isSensitiveConfigPath("channels.irc.nickserv.password")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("mapSensitivePaths", () => {
|
|
it("should detect sensitive fields nested inside all structural Zod types", () => {
|
|
const GrandSchema = z.object({
|
|
simple: z.string().register(sensitive).optional(),
|
|
simpleReversed: z.string().optional().register(sensitive),
|
|
nested: z.object({
|
|
nested: z.string().register(sensitive),
|
|
}),
|
|
list: z.array(z.string().register(sensitive)),
|
|
listOfObjects: z.array(z.object({ nested: z.string().register(sensitive) })),
|
|
headers: z.record(z.string(), z.string().register(sensitive)),
|
|
headersNested: z.record(z.string(), z.object({ nested: z.string().register(sensitive) })),
|
|
auth: z.union([
|
|
z.object({ type: z.literal("none") }),
|
|
z.object({ type: z.literal("token"), value: z.string().register(sensitive) }),
|
|
]),
|
|
merged: z
|
|
.object({ id: z.string() })
|
|
.and(z.object({ nested: z.string().register(sensitive) })),
|
|
});
|
|
|
|
const result = mapSensitivePaths(GrandSchema, "", {});
|
|
|
|
expect(result["simple"]?.sensitive).toBe(true);
|
|
expect(result["simpleReversed"]?.sensitive).toBe(true);
|
|
expect(result["nested.nested"]?.sensitive).toBe(true);
|
|
expect(result["list[]"]?.sensitive).toBe(true);
|
|
expect(result["listOfObjects[].nested"]?.sensitive).toBe(true);
|
|
expect(result["headers.*"]?.sensitive).toBe(true);
|
|
expect(result["headersNested.*.nested"]?.sensitive).toBe(true);
|
|
expect(result["auth.value"]?.sensitive).toBe(true);
|
|
expect(result["merged.nested"]?.sensitive).toBe(true);
|
|
});
|
|
|
|
it("should not detect non-sensitive fields nested inside all structural Zod types", () => {
|
|
const GrandSchema = z.object({
|
|
simple: z.string().optional(),
|
|
simpleReversed: z.string().optional(),
|
|
nested: z.object({
|
|
nested: z.string(),
|
|
}),
|
|
list: z.array(z.string()),
|
|
listOfObjects: z.array(z.object({ nested: z.string() })),
|
|
headers: z.record(z.string(), z.string()),
|
|
headersNested: z.record(z.string(), z.object({ nested: z.string() })),
|
|
auth: z.union([
|
|
z.object({ type: z.literal("none") }),
|
|
z.object({ type: z.literal("token"), value: z.string() }),
|
|
]),
|
|
merged: z.object({ id: z.string() }).and(z.object({ nested: z.string() })),
|
|
});
|
|
|
|
const result = mapSensitivePaths(GrandSchema, "", {});
|
|
|
|
expect(result["simple"]?.sensitive).toBe(undefined);
|
|
expect(result["simpleReversed"]?.sensitive).toBe(undefined);
|
|
expect(result["nested.nested"]?.sensitive).toBe(undefined);
|
|
expect(result["list[]"]?.sensitive).toBe(undefined);
|
|
expect(result["listOfObjects[].nested"]?.sensitive).toBe(undefined);
|
|
expect(result["headers.*"]?.sensitive).toBe(undefined);
|
|
expect(result["headersNested.*.nested"]?.sensitive).toBe(undefined);
|
|
expect(result["auth.value"]?.sensitive).toBe(undefined);
|
|
expect(result["merged.nested"]?.sensitive).toBe(undefined);
|
|
});
|
|
|
|
it("maps sensitive fields nested under object catchall schemas", () => {
|
|
const schema = z.object({
|
|
custom: z.object({}).catchall(
|
|
z.object({
|
|
apiKey: z.string().register(sensitive),
|
|
label: z.string(),
|
|
}),
|
|
),
|
|
});
|
|
|
|
const result = mapSensitivePaths(schema, "", {});
|
|
expect(result["custom.*.apiKey"]?.sensitive).toBe(true);
|
|
expect(result["custom.*.label"]?.sensitive).toBe(undefined);
|
|
});
|
|
|
|
it("does not mark plain catchall values sensitive by default", () => {
|
|
const schema = z.object({
|
|
env: z.object({}).catchall(z.string()),
|
|
});
|
|
|
|
const result = mapSensitivePaths(schema, "", {});
|
|
expect(result["env.*"]?.sensitive).toBe(undefined);
|
|
});
|
|
|
|
it("main schema yields correct hints (samples)", () => {
|
|
const schema = OpenClawSchema.toJSONSchema({
|
|
target: "draft-07",
|
|
unrepresentable: "any",
|
|
});
|
|
schema.title = "OpenClawConfig";
|
|
const hints = mapSensitivePaths(OpenClawSchema, "", {});
|
|
|
|
expect(hints["agents.defaults.memorySearch.remote.apiKey"]?.sensitive).toBe(true);
|
|
expect(hints["agents.list[].memorySearch.remote.apiKey"]?.sensitive).toBe(true);
|
|
expect(hints["channels.discord.accounts.*.token"]?.sensitive).toBe(true);
|
|
expect(hints["channels.googlechat.serviceAccount"]?.sensitive).toBe(true);
|
|
expect(hints["gateway.auth.token"]?.sensitive).toBe(true);
|
|
expect(hints["models.providers.*.headers.*"]?.sensitive).toBe(true);
|
|
expect(hints["skills.entries.*.apiKey"]?.sensitive).toBe(true);
|
|
});
|
|
});
|