fix(security): lock down inbound DMs by default

This commit is contained in:
Peter Steinberger
2026-01-06 17:51:38 +01:00
parent 327ad3c9c7
commit 967cef80bc
36 changed files with 2093 additions and 203 deletions

122
src/cli/pairing-cli.ts Normal file
View File

@@ -0,0 +1,122 @@
import type { Command } from "commander";
import { loadConfig } from "../config/config.js";
import { sendMessageDiscord } from "../discord/send.js";
import { sendMessageIMessage } from "../imessage/send.js";
import {
approveProviderPairingCode,
listProviderPairingRequests,
type PairingProvider,
} from "../pairing/pairing-store.js";
import { sendMessageSignal } from "../signal/send.js";
import { sendMessageSlack } from "../slack/send.js";
import { sendMessageTelegram } from "../telegram/send.js";
import { resolveTelegramToken } from "../telegram/token.js";
const PROVIDERS: PairingProvider[] = [
"telegram",
"signal",
"imessage",
"discord",
"slack",
"whatsapp",
];
function parseProvider(raw: unknown): PairingProvider {
const value = String(raw ?? "")
.trim()
.toLowerCase();
if ((PROVIDERS as string[]).includes(value)) return value as PairingProvider;
throw new Error(
`Invalid provider: ${value || "(empty)"} (expected one of: ${PROVIDERS.join(", ")})`,
);
}
async function notifyApproved(provider: PairingProvider, id: string) {
const message =
"✅ Clawdbot access approved. Send a message to start chatting.";
if (provider === "telegram") {
const cfg = loadConfig();
const { token } = resolveTelegramToken(cfg);
if (!token) throw new Error("telegram token not configured");
await sendMessageTelegram(id, message, { token });
return;
}
if (provider === "discord") {
await sendMessageDiscord(`user:${id}`, message);
return;
}
if (provider === "slack") {
await sendMessageSlack(`user:${id}`, message);
return;
}
if (provider === "signal") {
await sendMessageSignal(id, message);
return;
}
if (provider === "imessage") {
await sendMessageIMessage(id, message);
return;
}
// WhatsApp: approval still works (store); notifying requires an active web session.
}
export function registerPairingCli(program: Command) {
const pairing = program
.command("pairing")
.description("Secure DM pairing (approve inbound requests)");
pairing
.command("list")
.description("List pending pairing requests")
.requiredOption(
"--provider <provider>",
`Provider (${PROVIDERS.join(", ")})`,
)
.option("--json", "Print JSON", false)
.action(async (opts) => {
const provider = parseProvider(opts.provider);
const requests = await listProviderPairingRequests(provider);
if (opts.json) {
console.log(JSON.stringify({ provider, requests }, null, 2));
return;
}
if (requests.length === 0) {
console.log(`No pending ${provider} pairing requests.`);
return;
}
for (const r of requests) {
const meta = r.meta ? JSON.stringify(r.meta) : "";
console.log(
`${r.code} id=${r.id}${meta ? ` meta=${meta}` : ""} ${r.createdAt}`,
);
}
});
pairing
.command("approve")
.description("Approve a pairing code and allow that sender")
.requiredOption(
"--provider <provider>",
`Provider (${PROVIDERS.join(", ")})`,
)
.argument("<code>", "Pairing code (shown to the requester)")
.option("--notify", "Notify the requester on the same provider", false)
.action(async (code, opts) => {
const provider = parseProvider(opts.provider);
const approved = await approveProviderPairingCode({
provider,
code: String(code),
});
if (!approved) {
throw new Error(`No pending pairing request found for code: ${code}`);
}
console.log(`Approved ${provider} sender ${approved.id}.`);
if (!opts.notify) return;
await notifyApproved(provider, approved.id).catch((err) => {
console.log(`Failed to notify requester: ${String(err)}`);
});
});
}

View File

@@ -30,7 +30,9 @@ import { registerGatewayCli } from "./gateway-cli.js";
import { registerHooksCli } from "./hooks-cli.js";
import { registerModelsCli } from "./models-cli.js";
import { registerNodesCli } from "./nodes-cli.js";
import { registerPairingCli } from "./pairing-cli.js";
import { forceFreePort } from "./ports.js";
import { registerTelegramCli } from "./telegram-cli.js";
import { registerTuiCli } from "./tui-cli.js";
export { forceFreePort };
@@ -507,6 +509,8 @@ Examples:
registerCronCli(program);
registerDnsCli(program);
registerHooksCli(program);
registerPairingCli(program);
registerTelegramCli(program);
program
.command("status")

74
src/cli/telegram-cli.ts Normal file
View File

@@ -0,0 +1,74 @@
import type { Command } from "commander";
import { loadConfig } from "../config/config.js";
import {
approveTelegramPairingCode,
listTelegramPairingRequests,
} from "../telegram/pairing-store.js";
import { sendMessageTelegram } from "../telegram/send.js";
import { resolveTelegramToken } from "../telegram/token.js";
export function registerTelegramCli(program: Command) {
const telegram = program
.command("telegram")
.description("Telegram helpers (pairing, allowlists)");
const pairing = telegram
.command("pairing")
.description("Secure DM pairing (approve inbound requests)");
pairing
.command("list")
.description("List pending Telegram pairing requests")
.option("--json", "Print JSON", false)
.action(async (opts) => {
const requests = await listTelegramPairingRequests();
if (opts.json) {
console.log(JSON.stringify({ requests }, null, 2));
return;
}
if (requests.length === 0) {
console.log("No pending Telegram pairing requests.");
return;
}
for (const r of requests) {
const name = [r.firstName, r.lastName].filter(Boolean).join(" ").trim();
const username = r.username ? `@${r.username}` : "";
const who = [name, username].filter(Boolean).join(" ").trim();
console.log(
`${r.code} chatId=${r.chatId}${who ? ` ${who}` : ""} ${r.createdAt}`,
);
}
});
pairing
.command("approve")
.description("Approve a pairing code and allow that chatId")
.argument("<code>", "Pairing code (shown to the requester)")
.option("--no-notify", "Do not notify the requester on Telegram")
.action(async (code, opts) => {
const approved = await approveTelegramPairingCode({ code: String(code) });
if (!approved) {
throw new Error(`No pending pairing request found for code: ${code}`);
}
console.log(`Approved Telegram chatId ${approved.chatId}.`);
if (opts.notify === false) return;
const cfg = loadConfig();
const { token } = resolveTelegramToken(cfg);
if (!token) {
console.log(
"Telegram token not configured; skipping requester notification.",
);
return;
}
await sendMessageTelegram(
approved.chatId,
"✅ Clawdbot access approved. Send a message to start chatting.",
{ token },
).catch((err) => {
console.log(`Failed to notify requester: ${String(err)}`);
});
});
}