fix(imessage): normalize messaging targets (#1708)

Co-authored-by: Aaron Ng <1653630+aaronn@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-25 13:43:32 +00:00
parent a14ca1a337
commit 71eb6d5dd0
9 changed files with 140 additions and 42 deletions

View File

@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import { normalizeIMessageMessagingTarget } from "./imessage.js";
describe("imessage target normalization", () => {
it("preserves service prefixes for handles", () => {
expect(normalizeIMessageMessagingTarget("sms:+1 (555) 222-3333")).toBe("sms:+15552223333");
});
it("drops service prefixes for chat targets", () => {
expect(normalizeIMessageMessagingTarget("sms:chat_id:123")).toBe("chat_id:123");
expect(normalizeIMessageMessagingTarget("imessage:CHAT_GUID:abc")).toBe("chat_guid:abc");
expect(normalizeIMessageMessagingTarget("auto:ChatIdentifier:foo")).toBe("chat_identifier:foo");
});
});

View File

@@ -0,0 +1,35 @@
import { normalizeIMessageHandle } from "../../../imessage/targets.js";
// Service prefixes that indicate explicit delivery method; must be preserved during normalization
const SERVICE_PREFIXES = ["imessage:", "sms:", "auto:"] as const;
const CHAT_TARGET_PREFIX_RE =
/^(chat_id:|chatid:|chat:|chat_guid:|chatguid:|guid:|chat_identifier:|chatidentifier:|chatident:)/i;
export function normalizeIMessageMessagingTarget(raw: string): string | undefined {
const trimmed = raw.trim();
if (!trimmed) return undefined;
// Preserve service prefix if present (e.g., "sms:+1555" → "sms:+15551234567")
const lower = trimmed.toLowerCase();
for (const prefix of SERVICE_PREFIXES) {
if (lower.startsWith(prefix)) {
const remainder = trimmed.slice(prefix.length).trim();
const normalizedHandle = normalizeIMessageHandle(remainder);
if (!normalizedHandle) return undefined;
if (CHAT_TARGET_PREFIX_RE.test(normalizedHandle)) return normalizedHandle;
return `${prefix}${normalizedHandle}`;
}
}
const normalized = normalizeIMessageHandle(trimmed);
return normalized || undefined;
}
export function looksLikeIMessageTargetId(raw: string): boolean {
const trimmed = raw.trim();
if (!trimmed) return false;
if (/^(imessage:|sms:|auto:)/i.test(trimmed)) return true;
if (CHAT_TARGET_PREFIX_RE.test(trimmed)) return true;
if (trimmed.includes("@")) return true;
return /^\+?\d{3,}$/.test(trimmed);
}

View File

@@ -28,6 +28,27 @@ describe("imessage targets", () => {
expect(normalizeIMessageHandle(" +1 (555) 222-3333 ")).toBe("+15552223333");
});
it("normalizes chat_id prefixes case-insensitively", () => {
expect(normalizeIMessageHandle("CHAT_ID:123")).toBe("chat_id:123");
expect(normalizeIMessageHandle("Chat_Id:456")).toBe("chat_id:456");
expect(normalizeIMessageHandle("chatid:789")).toBe("chat_id:789");
expect(normalizeIMessageHandle("CHAT:42")).toBe("chat_id:42");
});
it("normalizes chat_guid prefixes case-insensitively", () => {
expect(normalizeIMessageHandle("CHAT_GUID:abc-def")).toBe("chat_guid:abc-def");
expect(normalizeIMessageHandle("ChatGuid:XYZ")).toBe("chat_guid:XYZ");
expect(normalizeIMessageHandle("GUID:test-guid")).toBe("chat_guid:test-guid");
});
it("normalizes chat_identifier prefixes case-insensitively", () => {
expect(normalizeIMessageHandle("CHAT_IDENTIFIER:iMessage;-;chat123")).toBe(
"chat_identifier:iMessage;-;chat123",
);
expect(normalizeIMessageHandle("ChatIdentifier:test")).toBe("chat_identifier:test");
expect(normalizeIMessageHandle("CHATIDENT:foo")).toBe("chat_identifier:foo");
});
it("checks allowFrom against chat_id", () => {
const ok = isAllowedIMessageSender({
allowFrom: ["chat_id:9"],

View File

@@ -34,6 +34,27 @@ export function normalizeIMessageHandle(raw: string): string {
if (lowered.startsWith("imessage:")) return normalizeIMessageHandle(trimmed.slice(9));
if (lowered.startsWith("sms:")) return normalizeIMessageHandle(trimmed.slice(4));
if (lowered.startsWith("auto:")) return normalizeIMessageHandle(trimmed.slice(5));
// Normalize chat_id/chat_guid/chat_identifier prefixes case-insensitively
for (const prefix of CHAT_ID_PREFIXES) {
if (lowered.startsWith(prefix)) {
const value = trimmed.slice(prefix.length).trim();
return `chat_id:${value}`;
}
}
for (const prefix of CHAT_GUID_PREFIXES) {
if (lowered.startsWith(prefix)) {
const value = trimmed.slice(prefix.length).trim();
return `chat_guid:${value}`;
}
}
for (const prefix of CHAT_IDENTIFIER_PREFIXES) {
if (lowered.startsWith(prefix)) {
const value = trimmed.slice(prefix.length).trim();
return `chat_identifier:${value}`;
}
}
if (trimmed.includes("@")) return trimmed.toLowerCase();
const normalized = normalizeE164(trimmed);
if (normalized) return normalized;

View File

@@ -197,12 +197,6 @@ export {
} from "../channels/plugins/setup-helpers.js";
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
export {
listIMessageAccountIds,
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
type ResolvedIMessageAccount,
} from "../imessage/accounts.js";
export type {
ChannelOnboardingAdapter,
@@ -210,7 +204,6 @@ export type {
} from "../channels/plugins/onboarding-types.js";
export { addWildcardAllowFrom, promptAccountId } from "../channels/plugins/onboarding/helpers.js";
export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js";
export { imessageOnboardingAdapter } from "../channels/plugins/onboarding/imessage.js";
export {
createActionGate,
@@ -264,6 +257,19 @@ export {
} from "../channels/plugins/normalize/discord.js";
export { collectDiscordStatusIssues } from "../channels/plugins/status-issues/discord.js";
// Channel: iMessage
export {
listIMessageAccountIds,
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
type ResolvedIMessageAccount,
} from "../imessage/accounts.js";
export { imessageOnboardingAdapter } from "../channels/plugins/onboarding/imessage.js";
export {
looksLikeIMessageTargetId,
normalizeIMessageMessagingTarget,
} from "../channels/plugins/normalize/imessage.js";
// Channel: Slack
export {
listEnabledSlackAccounts,