refactor: move whatsapp allowFrom config

This commit is contained in:
Peter Steinberger
2026-01-02 12:59:47 +01:00
parent 58d32d4542
commit 0766c5e3cb
39 changed files with 452 additions and 117 deletions

View File

@@ -488,3 +488,37 @@ describe("talk.voiceAliases", () => {
expect(res.ok).toBe(false);
});
});
describe("legacy config detection", () => {
it("rejects routing.allowFrom", async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({
routing: { allowFrom: ["+15555550123"] },
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues[0]?.path).toBe("routing.allowFrom");
}
});
it("surfaces legacy issues in snapshot", async () => {
await withTempHome(async (home) => {
const configPath = path.join(home, ".clawdis", "clawdis.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify({ routing: { allowFrom: ["+15555550123"] } }),
"utf-8",
);
vi.resetModules();
const { readConfigFileSnapshot } = await import("./config.js");
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(false);
expect(snap.legacyIssues.length).toBe(1);
expect(snap.legacyIssues[0]?.path).toBe("routing.allowFrom");
});
});
});

View File

@@ -58,6 +58,11 @@ export type WebConfig = {
reconnect?: WebReconnectConfig;
};
export type WhatsAppConfig = {
/** Optional allowlist for WhatsApp direct chats (E.164). */
allowFrom?: string[];
};
export type BrowserConfig = {
enabled?: boolean;
/** Base URL of the clawd browser control server. Default: http://127.0.0.1:18791 */
@@ -260,7 +265,6 @@ export type GroupChatConfig = {
};
export type RoutingConfig = {
allowFrom?: string[]; // E.164 numbers allowed to trigger auto-reply (without whatsapp:)
transcribeAudio?: {
// Optional CLI to turn inbound audio into text; templated args, must output transcript to stdout.
command: string[];
@@ -525,6 +529,7 @@ export type ClawdisConfig = {
messages?: MessagesConfig;
session?: SessionConfig;
web?: WebConfig;
whatsapp?: WhatsAppConfig;
telegram?: TelegramConfig;
discord?: DiscordConfig;
signal?: SignalConfig;
@@ -693,7 +698,6 @@ const HeartbeatSchema = z
const RoutingSchema = z
.object({
allowFrom: z.array(z.string()).optional(),
groupChat: GroupChatSchema,
transcribeAudio: TranscribeAudioSchema,
queue: z
@@ -909,6 +913,11 @@ const ClawdisSchema = z.object({
.optional(),
})
.optional(),
whatsapp: z
.object({
allowFrom: z.array(z.string()).optional(),
})
.optional(),
telegram: z
.object({
enabled: z.boolean().optional(),
@@ -1131,6 +1140,11 @@ export type ConfigValidationIssue = {
message: string;
};
export type LegacyConfigIssue = {
path: string;
message: string;
};
export type ConfigFileSnapshot = {
path: string;
exists: boolean;
@@ -1139,8 +1153,42 @@ export type ConfigFileSnapshot = {
valid: boolean;
config: ClawdisConfig;
issues: ConfigValidationIssue[];
legacyIssues: LegacyConfigIssue[];
};
type LegacyConfigRule = {
path: string[];
message: string;
};
const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
{
path: ["routing", "allowFrom"],
message:
"routing.allowFrom was removed; use whatsapp.allowFrom instead (run `clawdis doctor` to migrate).",
},
];
function findLegacyConfigIssues(raw: unknown): LegacyConfigIssue[] {
if (!raw || typeof raw !== "object") return [];
const root = raw as Record<string, unknown>;
const issues: LegacyConfigIssue[] = [];
for (const rule of LEGACY_CONFIG_RULES) {
let cursor: unknown = root;
for (const key of rule.path) {
if (!cursor || typeof cursor !== "object") {
cursor = undefined;
break;
}
cursor = (cursor as Record<string, unknown>)[key];
}
if (cursor !== undefined) {
issues.push({ path: rule.path.join("."), message: rule.message });
}
}
return issues;
}
function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -1199,6 +1247,16 @@ export function validateConfigObject(
):
| { ok: true; config: ClawdisConfig }
| { ok: false; issues: ConfigValidationIssue[] } {
const legacyIssues = findLegacyConfigIssues(raw);
if (legacyIssues.length > 0) {
return {
ok: false,
issues: legacyIssues.map((iss) => ({
path: iss.path,
message: iss.message,
})),
};
}
const validated = ClawdisSchema.safeParse(raw);
if (!validated.success) {
return {
@@ -1271,6 +1329,7 @@ export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
const exists = fs.existsSync(configPath);
if (!exists) {
const config = applyTalkApiKey({});
const legacyIssues: LegacyConfigIssue[] = [];
return {
path: configPath,
exists: false,
@@ -1279,6 +1338,7 @@ export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
valid: true,
config,
issues: [],
legacyIssues,
};
}
@@ -1296,9 +1356,12 @@ export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
issues: [
{ path: "", message: `JSON5 parse failed: ${parsedRes.error}` },
],
legacyIssues: [],
};
}
const legacyIssues = findLegacyConfigIssues(parsedRes.parsed);
const validated = validateConfigObject(parsedRes.parsed);
if (!validated.ok) {
return {
@@ -1309,6 +1372,7 @@ export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
valid: false,
config: {},
issues: validated.issues,
legacyIssues,
};
}
@@ -1320,6 +1384,7 @@ export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
valid: true,
config: applyTalkApiKey(validated.config),
issues: [],
legacyIssues,
};
} catch (err) {
return {
@@ -1330,6 +1395,7 @@ export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
valid: false,
config: {},
issues: [{ path: "", message: `read failed: ${String(err)}` }],
legacyIssues: [],
};
}
}