118 lines
3.5 KiB
TypeScript
118 lines
3.5 KiB
TypeScript
import { normalizeE164 } from "../utils.js";
|
|
|
|
export type SignalSender =
|
|
| { kind: "phone"; raw: string; e164: string }
|
|
| { kind: "uuid"; raw: string };
|
|
|
|
type SignalAllowEntry =
|
|
| { kind: "any" }
|
|
| { kind: "phone"; e164: string }
|
|
| { kind: "uuid"; raw: string };
|
|
|
|
const UUID_HYPHENATED_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
const UUID_COMPACT_RE = /^[0-9a-f]{32}$/i;
|
|
|
|
function looksLikeUuid(value: string): boolean {
|
|
if (UUID_HYPHENATED_RE.test(value) || UUID_COMPACT_RE.test(value)) {
|
|
return true;
|
|
}
|
|
const compact = value.replace(/-/g, "");
|
|
if (!/^[0-9a-f]+$/i.test(compact)) return false;
|
|
return /[a-f]/i.test(compact);
|
|
}
|
|
|
|
function stripSignalPrefix(value: string): string {
|
|
return value.replace(/^signal:/i, "").trim();
|
|
}
|
|
|
|
export function resolveSignalSender(params: {
|
|
sourceNumber?: string | null;
|
|
sourceUuid?: string | null;
|
|
}): SignalSender | null {
|
|
const sourceNumber = params.sourceNumber?.trim();
|
|
if (sourceNumber) {
|
|
return {
|
|
kind: "phone",
|
|
raw: sourceNumber,
|
|
e164: normalizeE164(sourceNumber),
|
|
};
|
|
}
|
|
const sourceUuid = params.sourceUuid?.trim();
|
|
if (sourceUuid) {
|
|
return { kind: "uuid", raw: sourceUuid };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function formatSignalSenderId(sender: SignalSender): string {
|
|
return sender.kind === "phone" ? sender.e164 : `uuid:${sender.raw}`;
|
|
}
|
|
|
|
export function formatSignalSenderDisplay(sender: SignalSender): string {
|
|
return sender.kind === "phone" ? sender.e164 : `uuid:${sender.raw}`;
|
|
}
|
|
|
|
export function formatSignalPairingIdLine(sender: SignalSender): string {
|
|
if (sender.kind === "phone") {
|
|
return `Your Signal number: ${sender.e164}`;
|
|
}
|
|
return `Your Signal sender id: ${formatSignalSenderId(sender)}`;
|
|
}
|
|
|
|
export function resolveSignalRecipient(sender: SignalSender): string {
|
|
return sender.kind === "phone" ? sender.e164 : sender.raw;
|
|
}
|
|
|
|
export function resolveSignalPeerId(sender: SignalSender): string {
|
|
return sender.kind === "phone" ? sender.e164 : `uuid:${sender.raw}`;
|
|
}
|
|
|
|
function parseSignalAllowEntry(entry: string): SignalAllowEntry | null {
|
|
const trimmed = entry.trim();
|
|
if (!trimmed) return null;
|
|
if (trimmed === "*") return { kind: "any" };
|
|
|
|
const stripped = stripSignalPrefix(trimmed);
|
|
const lower = stripped.toLowerCase();
|
|
if (lower.startsWith("uuid:")) {
|
|
const raw = stripped.slice("uuid:".length).trim();
|
|
if (!raw) return null;
|
|
return { kind: "uuid", raw };
|
|
}
|
|
|
|
if (looksLikeUuid(stripped)) {
|
|
return { kind: "uuid", raw: stripped };
|
|
}
|
|
|
|
return { kind: "phone", e164: normalizeE164(stripped) };
|
|
}
|
|
|
|
export function isSignalSenderAllowed(sender: SignalSender, allowFrom: string[]): boolean {
|
|
if (allowFrom.length === 0) return false;
|
|
const parsed = allowFrom
|
|
.map(parseSignalAllowEntry)
|
|
.filter((entry): entry is SignalAllowEntry => entry !== null);
|
|
if (parsed.some((entry) => entry.kind === "any")) return true;
|
|
return parsed.some((entry) => {
|
|
if (entry.kind === "phone" && sender.kind === "phone") {
|
|
return entry.e164 === sender.e164;
|
|
}
|
|
if (entry.kind === "uuid" && sender.kind === "uuid") {
|
|
return entry.raw === sender.raw;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
export function isSignalGroupAllowed(params: {
|
|
groupPolicy: "open" | "disabled" | "allowlist";
|
|
allowFrom: string[];
|
|
sender: SignalSender;
|
|
}): boolean {
|
|
const { groupPolicy, allowFrom, sender } = params;
|
|
if (groupPolicy === "disabled") return false;
|
|
if (groupPolicy === "open") return true;
|
|
if (allowFrom.length === 0) return false;
|
|
return isSignalSenderAllowed(sender, allowFrom);
|
|
}
|