diff --git a/src/channels/plugins/onboarding/signal.test.ts b/src/channels/plugins/onboarding/signal.test.ts new file mode 100644 index 00000000000..2c055e2ec94 --- /dev/null +++ b/src/channels/plugins/onboarding/signal.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { normalizeSignalAccountInput } from "./signal.js"; + +describe("normalizeSignalAccountInput", () => { + it("accepts already normalized numbers", () => { + expect(normalizeSignalAccountInput("+15555550123")).toBe("+15555550123"); + }); + + it("normalizes formatted input", () => { + expect(normalizeSignalAccountInput(" +1 (555) 000-1234 ")).toBe("+15550001234"); + }); + + it("rejects empty input", () => { + expect(normalizeSignalAccountInput(" ")).toBeNull(); + }); + + it("rejects non-numeric input", () => { + expect(normalizeSignalAccountInput("ok")).toBeNull(); + expect(normalizeSignalAccountInput("++--")).toBeNull(); + }); + + it("rejects numbers that are too short or too long", () => { + expect(normalizeSignalAccountInput("+1234")).toBeNull(); + expect(normalizeSignalAccountInput("+1234567890123456")).toBeNull(); + }); +}); diff --git a/src/channels/plugins/onboarding/signal.ts b/src/channels/plugins/onboarding/signal.ts index 3f5b969e5d5..168efec03fa 100644 --- a/src/channels/plugins/onboarding/signal.ts +++ b/src/channels/plugins/onboarding/signal.ts @@ -16,6 +16,23 @@ import { normalizeE164 } from "../../../utils.js"; import { addWildcardAllowFrom, promptAccountId } from "./helpers.js"; const channel = "signal" as const; +const MIN_E164_DIGITS = 5; +const MAX_E164_DIGITS = 15; +const INVALID_SIGNAL_ACCOUNT_ERROR = + "Invalid E.164 phone number (must start with + and country code, e.g. +15555550123)"; + +export function normalizeSignalAccountInput(value: string | null | undefined): string | null { + const trimmed = value?.trim(); + if (!trimmed) { + return null; + } + const normalized = normalizeE164(trimmed); + const digits = normalized.slice(1); + if (digits.length < MIN_E164_DIGITS || digits.length > MAX_E164_DIGITS) { + return null; + } + return normalized; +} function setSignalDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy) { const allowFrom = @@ -243,22 +260,34 @@ export const signalOnboardingAdapter: ChannelOnboardingAdapter = { let account = accountConfig.account ?? ""; if (account) { - const keep = await prompter.confirm({ - message: `Signal account set (${account}). Keep it?`, - initialValue: true, - }); - if (!keep) { + const normalizedExisting = normalizeSignalAccountInput(account); + if (!normalizedExisting) { + await prompter.note( + "Existing Signal account isn't a valid E.164 number. Please enter it again.", + "Signal", + ); account = ""; + } else { + account = normalizedExisting; + const keep = await prompter.confirm({ + message: `Signal account set (${account}). Keep it?`, + initialValue: true, + }); + if (!keep) account = ""; } } if (!account) { - account = String( + const rawAccount = String( await prompter.text({ message: "Signal bot number (E.164)", - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: (value) => + normalizeSignalAccountInput(String(value ?? "")) + ? undefined + : INVALID_SIGNAL_ACCOUNT_ERROR, }), - ).trim(); + ); + account = normalizeSignalAccountInput(rawAccount) ?? ""; } if (account) {