openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,440 lines • 53.1 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { l as normalizeE164 } from "./utils-CCC-BEJH.js";
import { t as createNonExitingRuntime } from "./runtime-B4lgFmsS.js";
import { a as shouldLogVerbose, r as logVerbose, t as danger } from "./globals-GTrXU4s9.js";
import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js";
import { u as fireAndForgetHook } from "./hook-runner-global-BnyQREr6.js";
import { m as triggerInternalHook, n as createInternalHookEvent } from "./internal-hooks-Bq6_9FFu.js";
import { c as kindFromMime, n as detectMime } from "./mime-C8mVE2Bw.js";
import { u as resolveStorePath } from "./paths-NEwU8m3X.js";
import { o as readSessionUpdatedAt } from "./store-Qsgtu-0y.js";
import { n as estimateBase64DecodedBytes } from "./base64-l6yrCyHc.js";
import { u as saveMediaBuffer } from "./store-C9M_0A1p.js";
import { t as hasControlCommand } from "./command-detection-iKcBJ8it.js";
import { a as enqueueSystemEvent } from "./system-events-C5WI3S5a.js";
import { n as sleepWithAbort, t as computeBackoff } from "./backoff-S1yazmy1.js";
import { i as toInternalMessageReceivedContext } from "./message-hook-mappers-BqsqHKq-.js";
import { i as deliverTextOrMediaReply, m as resolveSendableOutboundReplyParts } from "./reply-payload-D-VMfpYI.js";
import { a as settleReplyDispatcher, t as dispatchInboundMessage } from "./dispatch-DO0Fpkbp.js";
import { n as resolveChannelGroupRequireMention, t as resolveChannelGroupPolicy } from "./group-policy-CShy7eWv.js";
import { a as resolveInboundLastRouteSessionKey, i as resolveAgentRoute } from "./resolve-route-xcIS8NFL.js";
import { a as warnMissingProviderGroupPolicyFallbackOnce, n as resolveAllowlistProviderRuntimeGroupPolicy, r as resolveDefaultGroupPolicy } from "./runtime-group-policy-CSD1Is8G.js";
import { l as resolvePinnedMainDmOwnerFromAllowlist } from "./dm-policy-shared-BjnswXJt.js";
import { i as resolveHumanDelayConfig } from "./identity-Qcvvo6SY.js";
import { c as resolveTextChunkLimit, o as chunkTextWithMode, s as resolveChunkMode } from "./chunk-Cpb8JO1x.js";
import "./media-runtime-fgpmX6eL.js";
import "./runtime-config-snapshot-CDoFLjqb.js";
import "./runtime-env-D_2q8-VK.js";
import "./security-runtime-CQm7DD1u.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import "./text-utility-runtime-BZ3jGr-e.js";
import { n as buildMentionRegexes, r as matchesMentionPatterns } from "./mentions-Be0DZkGs.js";
import { r as createReplyDispatcherWithTyping } from "./reply-dispatcher.types-CKL81tAi.js";
import "./routing-CQ63ICPX.js";
import { a as resolveEnvelopeFormatOptions, i as formatInboundFromLabel, r as formatInboundEnvelope } from "./envelope-IMcsY0tl.js";
import { b as filterChannelInboundQuoteContext, y as buildChannelInboundEventContext } from "./kernel-BdraY_qZ.js";
import { n as resolveInboundMentionDecision } from "./mention-gating-3P8aSD7o.js";
import { t as recordInboundSession } from "./session-Do0yFgU8.js";
import "./history-6XlO5h6s.js";
import { t as createChannelReplyPipeline } from "./reply-pipeline-eJPstCAz.js";
import { t as createChannelHistoryWindow } from "./history-window-B0hyPVKC.js";
import { d as upsertChannelPairingRequest } from "./pairing-store-CxANz_ON.js";
import "./channel-policy-CIW58SA5.js";
import "./reply-history-Y9HXucO9.js";
import { t as CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "./approval-handler-adapter-runtime-DLWtIqHq.js";
import { r as registerChannelRuntimeContext } from "./channel-runtime-context-Bx08uB94.js";
import { t as resolveChannelContextVisibilityMode } from "./context-visibility-D4NM8ac1.js";
import "./reply-runtime--Kbd63wS.js";
import { f as runChannelInboundEvent } from "./inbound-reply-dispatch-BpzI7GIp.js";
import "./channel-outbound-B3_Zy-kG.js";
import { n as logInboundDrop, r as logTypingFailure } from "./logging-gUWPKC5g.js";
import { t as waitForTransportReady } from "./transport-ready-CPbvF2Ea.js";
import "./system-event-runtime-D80J5tzu.js";
import "./transport-ready-runtime-DNcaz8UY.js";
import "./conversation-runtime-VjM1-D7a.js";
import "./agent-runtime-CvpQRNVf.js";
import "./hook-runtime-CA9YrfdF.js";
import "./command-auth-native-D4ML0UKp.js";
import { c as createChannelInboundDebouncer, l as shouldDebounceTextInbound } from "./channel-inbound-bl7VmTdr.js";
import "./channel-feedback-Bi1IYmE_.js";
import { n as createChannelIngressResolver, o as defineStableChannelIngressIdentity } from "./message-access-mwxdTUGC.js";
import "./channel-ingress-runtime-rGltVkKC.js";
import { t as createChannelPairingChallengeIssuer } from "./channel-pairing-Bk6_JhTm.js";
import "./session-store-runtime-rvUx2Fil.js";
import { i as resolveSignalAccount } from "./accounts-ms-4J9iY.js";
import { a as normalizeSignalAllowRecipient, c as resolveSignalSender, d as normalizeSignalMessagingTarget, i as isSignalSenderAllowed, l as looksLikeUuid, n as formatSignalSenderDisplay, o as resolveSignalPeerId, r as formatSignalSenderId, s as resolveSignalRecipient, t as formatSignalPairingIdLine } from "./identity-IGz-X4Li.js";
import { t as isSignalNativeApprovalHandlerConfigured } from "./approval-native-DiTxhcNB.js";
import { n as signalRpcRequest, r as streamSignalEvents, t as signalCheck } from "./client-adapter-Bk8RFbS1.js";
import { a as maybeResolveSignalApprovalReaction, n as sendReadReceiptSignal, r as sendTypingSignal, s as resolveSignalApprovalConversationKey, t as sendMessageSignal } from "./send-hICEM6a-.js";
import path from "node:path";
import os from "node:os";
import { spawn } from "node:child_process";
//#region extensions/signal/src/daemon.ts
function formatSignalDaemonExit(exit) {
return `signal daemon exited (source=${exit.source} code=${exit.code ?? "null"} signal=${exit.signal ?? "null"})`;
}
function isRecoverableSignalCliReceiveException(line) {
return /\breceive exception:\s+.*\binvalid PreKey message:\s+decryption failed\b/i.test(line);
}
function classifySignalCliLogLine(line) {
const trimmed = line.trim();
if (!trimmed) return null;
if (/\bERROR\b/.test(trimmed)) return "error";
if (isRecoverableSignalCliReceiveException(trimmed)) return "log";
if (/\b(FAILED|SEVERE|EXCEPTION)\b/i.test(trimmed)) return "error";
return "log";
}
function bindSignalCliOutput(params) {
params.stream?.on("data", (data) => {
for (const line of data.toString().split(/\r?\n/)) {
const kind = classifySignalCliLogLine(line);
if (kind === "log") params.log(`signal-cli: ${line.trim()}`);
else if (kind === "error") params.error(`signal-cli: ${line.trim()}`);
}
});
}
function resolveSignalCliConfigPath(raw) {
const value = raw.trim();
if (value === "~") return os.homedir();
if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(os.homedir(), value.slice(2));
return value;
}
function buildDaemonArgs(opts) {
const args = [];
if (opts.configPath?.trim()) args.push("--config", resolveSignalCliConfigPath(opts.configPath));
if (opts.account) args.push("-a", opts.account);
args.push("daemon");
args.push("--http", `${opts.httpHost}:${opts.httpPort}`);
args.push("--no-receive-stdout");
if (opts.receiveMode) args.push("--receive-mode", opts.receiveMode);
if (opts.ignoreAttachments) args.push("--ignore-attachments");
if (opts.ignoreStories) args.push("--ignore-stories");
if (opts.sendReadReceipts) args.push("--send-read-receipts");
return args;
}
function spawnSignalDaemon(opts) {
const args = buildDaemonArgs(opts);
const child = spawn(opts.cliPath, args, { stdio: [
"ignore",
"pipe",
"pipe"
] });
const log = opts.runtime?.log ?? (() => {});
const error = opts.runtime?.error ?? (() => {});
let exited = false;
let settledExit = false;
let resolveExit;
const exitedPromise = new Promise((resolve) => {
resolveExit = resolve;
});
const settleExit = (value) => {
if (settledExit) return;
settledExit = true;
exited = true;
resolveExit(value);
};
bindSignalCliOutput({
stream: child.stdout,
log,
error
});
bindSignalCliOutput({
stream: child.stderr,
log,
error
});
child.once("exit", (code, signal) => {
settleExit({
source: "process",
code: typeof code === "number" ? code : null,
signal: signal ?? null
});
error(formatSignalDaemonExit({
source: "process",
code: code ?? null,
signal: signal ?? null
}));
});
child.once("close", (code, signal) => {
settleExit({
source: "process",
code: typeof code === "number" ? code : null,
signal: signal ?? null
});
});
child.on("error", (err) => {
error(`signal-cli spawn error: ${String(err)}`);
settleExit({
source: "spawn-error",
code: null,
signal: null
});
});
return {
pid: child.pid ?? void 0,
exited: exitedPromise,
isExited: () => exited,
stop: () => {
if (!child.killed && !exited) child.kill("SIGTERM");
}
};
}
//#endregion
//#region extensions/signal/src/monitor/access-policy.ts
const SIGNAL_UUID_KIND = "plugin:signal-uuid";
const SIGNAL_GROUP_KIND = "plugin:signal-group";
function strippedSignalEntry(entry) {
const trimmed = entry.trim();
if (!trimmed) return null;
const signalStripped = trimmed.replace(/^signal:/i, "").trim();
return {
trimmed,
signalStripped,
lower: signalStripped.toLowerCase()
};
}
function normalizeSignalGroupEntry(entry) {
const parsed = strippedSignalEntry(entry);
if (!parsed) return null;
const { trimmed, signalStripped, lower } = parsed;
if (lower.startsWith("group:")) return signalStripped.slice(6).trim() || null;
return trimmed;
}
function normalizeSignalUuidEntry(entry) {
const parsed = strippedSignalEntry(entry);
if (!parsed) return null;
const { signalStripped, lower } = parsed;
if (lower.startsWith("uuid:")) return signalStripped.slice(5).trim() || null;
return looksLikeUuid(signalStripped) ? signalStripped : null;
}
function normalizeSignalPhoneEntry(entry) {
const parsed = strippedSignalEntry(entry);
if (!parsed) return null;
return normalizeSignalAllowRecipient(parsed.trimmed) ?? null;
}
const signalIngressIdentity = defineStableChannelIngressIdentity({
key: "stable",
normalizeEntry: () => null,
aliases: [
{
key: "phone",
kind: "phone",
normalizeEntry: normalizeSignalPhoneEntry,
normalizeSubject: (value) => value,
sensitivity: "pii"
},
{
key: "uuid",
kind: SIGNAL_UUID_KIND,
normalizeEntry: normalizeSignalUuidEntry,
normalizeSubject: (value) => value,
sensitivity: "pii"
},
{
key: "group",
kind: SIGNAL_GROUP_KIND,
normalizeEntry: normalizeSignalGroupEntry,
normalizeSubject: (value) => value
}
],
isWildcardEntry: (entry) => entry.trim() === "*",
resolveEntryId({ entryIndex, fieldKey }) {
return `entry-${entryIndex + 1}:${fieldKey}`;
}
});
function signalSubjectInput(params) {
return {
stableId: formatSignalSenderId(params.sender),
aliases: {
phone: params.sender.kind === "phone" ? params.sender.e164 : void 0,
uuid: params.sender.kind === "uuid" ? params.sender.raw : void 0,
group: params.groupId
}
};
}
async function resolveSignalAccessState(params) {
const isGroup = params.isGroup ?? params.groupId != null;
const command = params.hasControlCommand === true ? {
allowTextCommands: true,
directGroupAllowFrom: "effective"
} : void 0;
return await createChannelIngressResolver({
channelId: "signal",
accountId: params.accountId,
identity: signalIngressIdentity,
cfg: params.cfg,
...params.readStoreAllowFrom ? { readStoreAllowFrom: params.readStoreAllowFrom } : {},
useDefaultPairingStore: params.readStoreAllowFrom == null
}).message({
subject: signalSubjectInput({
sender: params.sender,
groupId: isGroup ? params.groupId : void 0
}),
conversation: {
kind: isGroup ? "group" : "direct",
id: isGroup ? params.groupId ?? "unknown" : params.sender.raw
},
...isGroup ? { event: { mayPair: false } } : {},
dmPolicy: params.dmPolicy,
groupPolicy: params.groupPolicy,
policy: { groupAllowFromFallbackToAllowFrom: true },
allowFrom: params.allowFrom,
groupAllowFrom: params.groupAllowFrom,
command
});
}
async function handleSignalDirectMessageAccess(params) {
if (params.dmAccessDecision === "allow") return true;
if (params.dmAccessDecision === "block") {
if (params.dmPolicy !== "disabled") params.log(`Blocked signal sender ${params.senderDisplay} (dmPolicy=${params.dmPolicy})`);
return false;
}
if (params.dmPolicy === "pairing") await createChannelPairingChallengeIssuer({
channel: "signal",
upsertPairingRequest: async ({ id, meta }) => await upsertChannelPairingRequest({
channel: "signal",
id,
accountId: params.accountId,
meta
})
})({
senderId: params.senderId,
senderIdLine: params.senderIdLine,
meta: { name: params.senderName },
sendPairingReply: params.sendPairingReply,
onCreated: () => {
params.log(`signal pairing request sender=${params.senderId}`);
},
onReplyError: (err) => {
params.log(`signal pairing reply failed for ${params.senderId}: ${String(err)}`);
}
});
return false;
}
//#endregion
//#region extensions/signal/src/monitor/inbound-context.ts
function resolveSignalQuoteContext(params) {
const contextVisibilityMode = resolveChannelContextVisibilityMode({
cfg: params.cfg,
channel: "signal",
accountId: params.accountId
});
const quoteText = normalizeOptionalString(params.dataMessage?.quote?.text) ?? "";
const quoteSender = resolveSignalSender({
sourceNumber: params.dataMessage?.quote?.author ?? null,
sourceUuid: params.dataMessage?.quote?.authorUuid ?? null
});
const quoteSenderAllowed = !params.isGroup || params.effectiveGroupAllow.length === 0 ? true : quoteSender ? isSignalSenderAllowed(quoteSender, params.effectiveGroupAllow) : false;
const visibleQuote = filterChannelInboundQuoteContext(contextVisibilityMode, {
body: quoteText,
sender: quoteSender ? formatSignalSenderDisplay(quoteSender) : void 0,
senderAllowed: quoteSenderAllowed,
isQuote: true
});
return {
contextVisibilityMode,
decision: {
include: Boolean(visibleQuote),
reason: visibleQuote ? contextVisibilityMode === "all" ? "mode_all" : quoteSenderAllowed ? "sender_allowed" : "quote_override" : "blocked"
},
quoteSenderAllowed,
visibleQuoteText: visibleQuote?.body ?? "",
visibleQuoteSender: visibleQuote?.sender
};
}
//#endregion
//#region extensions/signal/src/monitor/mentions.ts
const OBJECT_REPLACEMENT = "";
function isValidMention(mention) {
if (!mention) return false;
if (!(mention.uuid || mention.number)) return false;
if (typeof mention.start !== "number" || Number.isNaN(mention.start)) return false;
if (typeof mention.length !== "number" || Number.isNaN(mention.length)) return false;
return mention.length > 0;
}
function clampBounds(start, length, textLength) {
const safeStart = Math.max(0, Math.trunc(start));
return {
start: safeStart,
end: Math.min(textLength, safeStart + Math.max(0, Math.trunc(length)))
};
}
function renderSignalMentions(message, mentions) {
if (!message || !mentions?.length) return message;
let normalized = message;
const candidates = mentions.filter(isValidMention).toSorted((a, b) => b.start - a.start);
for (const mention of candidates) {
const identifier = mention.uuid ?? mention.number;
if (!identifier) continue;
const { start, end } = clampBounds(mention.start, mention.length, normalized.length);
if (start >= end) continue;
if (!normalized.slice(start, end).includes(OBJECT_REPLACEMENT)) continue;
normalized = normalized.slice(0, start) + `@${identifier}` + normalized.slice(end);
}
return normalized;
}
//#endregion
//#region extensions/signal/src/monitor/event-handler.ts
function formatAttachmentKindCount(kind, count) {
if (kind === "attachment") return `${count} file${count > 1 ? "s" : ""}`;
return `${count} ${kind}${count > 1 ? "s" : ""}`;
}
function formatAttachmentSummaryPlaceholder(contentTypes) {
const kindCounts = /* @__PURE__ */ new Map();
for (const contentType of contentTypes) {
const kind = kindFromMime(contentType) ?? "attachment";
kindCounts.set(kind, (kindCounts.get(kind) ?? 0) + 1);
}
return `[${[...kindCounts.entries()].map(([kind, count]) => formatAttachmentKindCount(kind, count)).join(" + ")} attached]`;
}
function resolveSignalInboundRoute(params) {
return resolveAgentRoute({
cfg: params.cfg,
channel: "signal",
accountId: params.accountId,
peer: {
kind: params.isGroup ? "group" : "direct",
id: params.isGroup ? params.groupId ?? "unknown" : params.senderPeerId
}
});
}
function createSignalEventHandler(deps) {
async function handleSignalInboundMessage(entry) {
const fromLabel = formatInboundFromLabel({
isGroup: entry.isGroup,
groupLabel: entry.groupName ?? void 0,
groupId: entry.groupId ?? "unknown",
groupFallback: "Group",
directLabel: entry.senderName,
directId: entry.senderDisplay
});
const route = resolveSignalInboundRoute({
cfg: deps.cfg,
accountId: deps.accountId,
isGroup: entry.isGroup,
groupId: entry.groupId,
senderPeerId: entry.senderPeerId
});
const storePath = resolveStorePath(deps.cfg.session?.store, { agentId: route.agentId });
const envelopeOptions = resolveEnvelopeFormatOptions(deps.cfg);
const previousTimestamp = readSessionUpdatedAt({
storePath,
sessionKey: route.sessionKey
});
const body = formatInboundEnvelope({
channel: "Signal",
from: fromLabel,
timestamp: entry.timestamp ?? void 0,
body: entry.bodyText,
chatType: entry.isGroup ? "group" : "direct",
sender: {
name: entry.senderName,
id: entry.senderDisplay
},
previousTimestamp,
envelope: envelopeOptions
});
let combinedBody = body;
const historyKey = entry.isGroup ? entry.groupId ?? "unknown" : void 0;
if (entry.isGroup && historyKey) combinedBody = createChannelHistoryWindow({ historyMap: deps.groupHistories }).buildPendingContext({
historyKey,
limit: deps.historyLimit,
currentMessage: combinedBody,
formatEntry: (historyEntry) => formatInboundEnvelope({
channel: "Signal",
from: fromLabel,
timestamp: historyEntry.timestamp,
body: `${historyEntry.body}${historyEntry.messageId ? ` [id:${historyEntry.messageId}]` : ""}`,
chatType: "group",
senderLabel: historyEntry.sender,
envelope: envelopeOptions
})
});
const signalToRaw = entry.isGroup ? `group:${entry.groupId}` : `signal:${entry.senderRecipient}`;
const signalTo = normalizeSignalMessagingTarget(signalToRaw) ?? signalToRaw;
const inboundHistory = entry.isGroup && historyKey && deps.historyLimit > 0 ? createChannelHistoryWindow({ historyMap: deps.groupHistories }).buildInboundHistory({
historyKey,
limit: deps.historyLimit
}) : void 0;
const media = entry.mediaPaths && entry.mediaPaths.length > 0 ? entry.mediaPaths.map((path, index) => ({
path,
url: path,
contentType: entry.mediaTypes?.[index]
})) : entry.mediaPath ? [{
path: entry.mediaPath,
url: entry.mediaPath,
contentType: entry.mediaType
}] : void 0;
const ctxPayload = buildChannelInboundEventContext({
channel: "signal",
supplemental: { quote: entry.replyToBody ? {
body: entry.replyToBody,
sender: entry.replyToSender,
isQuote: entry.replyToIsQuote
} : void 0 },
messageId: entry.messageId,
timestamp: entry.timestamp ?? void 0,
from: entry.isGroup ? `group:${entry.groupId ?? "unknown"}` : `signal:${entry.senderRecipient}`,
sender: {
id: entry.senderDisplay,
name: entry.senderName
},
conversation: {
kind: entry.isGroup ? "group" : "direct",
id: entry.isGroup ? entry.groupId ?? "unknown" : entry.senderRecipient,
label: fromLabel
},
route: {
agentId: route.agentId,
accountId: route.accountId,
routeSessionKey: route.sessionKey
},
reply: { to: signalTo },
message: {
body: combinedBody,
bodyForAgent: entry.bodyText,
inboundHistory,
rawBody: entry.bodyText,
commandBody: entry.commandBody
},
access: {
...entry.isGroup ? { mentions: {
canDetectMention: true,
wasMentioned: entry.wasMentioned === true
} } : {},
commands: { authorized: entry.commandAuthorized }
},
media,
extra: { GroupSubject: entry.isGroup ? entry.groupName ?? void 0 : void 0 }
});
if (shouldLogVerbose()) {
const preview = body.slice(0, 200).replace(/\\n/g, "\\\\n");
logVerbose(`signal inbound: from=${ctxPayload.From} len=${body.length} preview="${preview}"`);
}
const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({
cfg: deps.cfg,
agentId: route.agentId,
channel: "signal",
accountId: route.accountId,
typing: {
start: async () => {
if (!ctxPayload.To) return;
await sendTypingSignal(ctxPayload.To, {
cfg: deps.cfg,
baseUrl: deps.baseUrl,
account: deps.account,
accountId: deps.accountId
});
},
onStartError: (err) => {
logTypingFailure({
log: logVerbose,
channel: "signal",
target: ctxPayload.To ?? void 0,
error: err
});
}
}
});
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
...replyPipeline,
humanDelay: resolveHumanDelayConfig(deps.cfg, route.agentId),
typingCallbacks,
deliver: async (payload, _info) => {
await deps.deliverReplies({
cfg: deps.cfg,
replies: [payload],
target: ctxPayload.To,
baseUrl: deps.baseUrl,
account: deps.account,
accountId: deps.accountId,
runtime: deps.runtime,
maxBytes: deps.mediaMaxBytes,
textLimit: deps.textLimit
});
},
onError: (err, info) => {
deps.runtime.error?.(danger(`signal ${info.kind} reply failed: ${String(err)}`));
}
});
const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
route,
sessionKey: route.sessionKey
});
await runChannelInboundEvent({
channel: "signal",
accountId: route.accountId,
raw: entry,
adapter: {
ingest: () => ({
id: entry.messageId ?? `${entry.timestamp ?? Date.now()}`,
timestamp: entry.timestamp,
rawText: entry.bodyText,
raw: entry
}),
resolveTurn: () => ({
channel: "signal",
accountId: route.accountId,
routeSessionKey: route.sessionKey,
storePath,
ctxPayload,
recordInboundSession,
record: {
updateLastRoute: !entry.isGroup ? {
sessionKey: inboundLastRouteSessionKey,
channel: "signal",
to: entry.senderRecipient,
accountId: route.accountId,
mainDmOwnerPin: (() => {
if (inboundLastRouteSessionKey !== route.mainSessionKey) return;
const pinnedOwner = resolvePinnedMainDmOwnerFromAllowlist({
dmScope: deps.cfg.session?.dmScope,
allowFrom: deps.allowFrom,
normalizeEntry: normalizeSignalAllowRecipient
});
if (!pinnedOwner) return;
return {
ownerRecipient: pinnedOwner,
senderRecipient: entry.senderRecipient,
onSkip: ({ ownerRecipient, senderRecipient }) => {
logVerbose(`signal: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`);
}
};
})()
} : void 0,
onRecordError: (err) => {
logVerbose(`signal: failed updating session meta: ${String(err)}`);
}
},
history: {
isGroup: entry.isGroup,
historyKey,
historyMap: deps.groupHistories,
limit: deps.historyLimit
},
onPreDispatchFailure: () => settleReplyDispatcher({
dispatcher,
onSettled: () => markDispatchIdle()
}),
runDispatch: async () => {
try {
return await dispatchInboundMessage({
ctx: ctxPayload,
cfg: deps.cfg,
dispatcher,
replyOptions: {
...replyOptions,
disableBlockStreaming: typeof deps.blockStreaming === "boolean" ? !deps.blockStreaming : void 0,
onModelSelected
}
});
} finally {
markDispatchIdle();
}
}
})
}
});
}
const { debouncer: inboundDebouncer } = createChannelInboundDebouncer({
cfg: deps.cfg,
channel: "signal",
buildKey: (entry) => {
const conversationId = entry.isGroup ? entry.groupId ?? "unknown" : entry.senderPeerId;
if (!conversationId || !entry.senderPeerId) return null;
return `signal:${deps.accountId}:${conversationId}:${entry.senderPeerId}`;
},
shouldDebounce: (entry) => {
return shouldDebounceTextInbound({
text: entry.bodyText,
cfg: deps.cfg,
hasMedia: Boolean(entry.mediaPath || entry.mediaType || entry.mediaPaths?.length)
});
},
onFlush: async (entries) => {
const last = entries.at(-1);
if (!last) return;
if (entries.length === 1) {
await handleSignalInboundMessage(last);
return;
}
const combinedText = entries.map((entry) => entry.bodyText).filter(Boolean).join("\\n");
if (!combinedText.trim()) return;
await handleSignalInboundMessage({
...last,
bodyText: combinedText,
mediaPath: void 0,
mediaType: void 0,
mediaPaths: void 0,
mediaTypes: void 0
});
},
onError: (err) => {
deps.runtime.error?.(`signal debounce flush failed: ${String(err)}`);
}
});
async function handleReactionOnlyInbound(params) {
if (params.hasBodyContent) return false;
if (params.reaction.isRemove) return true;
const emojiLabel = normalizeOptionalString(params.reaction.emoji) ?? "emoji";
const senderName = params.envelope.sourceName ?? params.senderDisplay;
logVerbose(`signal reaction: ${emojiLabel} from ${senderName}`);
const groupId = params.reaction.groupInfo?.groupId ?? void 0;
const groupName = params.reaction.groupInfo?.groupName ?? void 0;
const isGroup = Boolean(groupId);
const messageId = params.reaction.targetSentTimestamp ? String(params.reaction.targetSentTimestamp) : "unknown";
const conversationKey = resolveSignalApprovalConversationKey(groupId ? `group:${groupId}` : `signal:${resolveSignalRecipient(params.sender)}`);
if (conversationKey && await maybeResolveSignalApprovalReaction({
cfg: deps.cfg,
accountId: deps.accountId,
conversationKey,
messageId,
reactionKey: emojiLabel,
actorId: formatSignalSenderId(params.sender),
targetAuthor: params.reaction.targetAuthor,
targetAuthorUuid: params.reaction.targetAuthorUuid,
logVerboseMessage: logVerbose
})) return true;
if (params.accessDecision.decision !== "allow") {
logVerbose(`Blocked signal reaction sender ${params.senderDisplay} (${params.accessDecision.reasonCode})`);
return true;
}
const targets = deps.resolveSignalReactionTargets(params.reaction);
if (!deps.shouldEmitSignalReactionNotification({
mode: deps.reactionMode,
account: deps.account,
targets,
sender: params.sender,
allowlist: deps.reactionAllowlist
})) return true;
const senderPeerId = resolveSignalPeerId(params.sender);
const route = resolveSignalInboundRoute({
cfg: deps.cfg,
accountId: deps.accountId,
isGroup,
groupId,
senderPeerId
});
const groupLabel = isGroup ? `${groupName ?? "Signal Group"} id:${groupId}` : void 0;
const text = deps.buildSignalReactionSystemEventText({
emojiLabel,
actorLabel: senderName,
messageId,
targetLabel: targets[0]?.display,
groupLabel
});
const contextKey = [
"signal",
"reaction",
"added",
messageId,
formatSignalSenderId(params.sender),
emojiLabel,
groupId ?? ""
].filter(Boolean).join(":");
enqueueSystemEvent(text, {
sessionKey: route.sessionKey,
contextKey
});
return true;
}
return async (event) => {
if (event.event !== "receive" || !event.data) return;
let payload;
try {
payload = JSON.parse(event.data);
} catch (err) {
deps.runtime.error?.(`failed to parse event: ${String(err)}`);
return;
}
if (payload?.exception?.message) deps.runtime.error?.(`receive exception: ${payload.exception.message}`);
const envelope = payload?.envelope;
if (!envelope) return;
const sender = resolveSignalSender(envelope);
if (!sender) return;
const normalizedAccount = deps.account ? normalizeE164(deps.account) : void 0;
if (sender.kind === "phone" && normalizedAccount != null && sender.e164 === normalizedAccount || sender.kind === "uuid" && deps.accountUuid != null && sender.raw === deps.accountUuid) return;
if ("syncMessage" in envelope) return;
const dataMessage = envelope.dataMessage ?? envelope.editMessage?.dataMessage;
const reaction = deps.isSignalReactionMessage(envelope.reactionMessage) ? envelope.reactionMessage : deps.isSignalReactionMessage(dataMessage?.reaction) ? dataMessage?.reaction : null;
const messageText = renderSignalMentions(dataMessage?.message ?? "", dataMessage?.mentions).trim();
const groupId = dataMessage?.groupInfo?.groupId ?? reaction?.groupInfo?.groupId ?? void 0;
const isGroup = Boolean(groupId);
const hasControlCommandInMessage = hasControlCommand(messageText, deps.cfg);
const senderDisplay = formatSignalSenderDisplay(sender);
const { senderAccess, commandAccess } = await resolveSignalAccessState({
accountId: deps.accountId,
dmPolicy: deps.dmPolicy,
groupPolicy: deps.groupPolicy,
allowFrom: deps.allowFrom,
groupAllowFrom: deps.groupAllowFrom,
sender,
groupId,
isGroup,
cfg: deps.cfg,
hasControlCommand: hasControlCommandInMessage
});
const quoteText = normalizeOptionalString(dataMessage?.quote?.text) ?? "";
const { contextVisibilityMode, quoteSenderAllowed, visibleQuoteText, visibleQuoteSender } = resolveSignalQuoteContext({
cfg: deps.cfg,
accountId: deps.accountId,
isGroup,
dataMessage,
effectiveGroupAllow: senderAccess.effectiveGroupAllowFrom
});
if (quoteText && !visibleQuoteText && isGroup) logVerbose(`signal: drop quote context (mode=${contextVisibilityMode}, sender_allowed=${quoteSenderAllowed ? "yes" : "no"})`);
const hasBodyContent = Boolean(messageText || visibleQuoteText) || Boolean(!reaction && dataMessage?.attachments?.length);
if (reaction && await handleReactionOnlyInbound({
envelope,
sender,
senderDisplay,
reaction,
hasBodyContent,
accessDecision: senderAccess
})) return;
if (!dataMessage) return;
const senderRecipient = resolveSignalRecipient(sender);
const senderPeerId = resolveSignalPeerId(sender);
const senderAllowId = formatSignalSenderId(sender);
if (!senderRecipient) return;
const senderIdLine = formatSignalPairingIdLine(sender);
const groupName = dataMessage.groupInfo?.groupName ?? void 0;
if (!isGroup) {
if (!await handleSignalDirectMessageAccess({
dmPolicy: deps.dmPolicy,
dmAccessDecision: senderAccess.decision,
senderId: senderAllowId,
senderIdLine,
senderDisplay,
senderName: envelope.sourceName ?? void 0,
accountId: deps.accountId,
sendPairingReply: async (text) => {
await sendMessageSignal(`signal:${senderRecipient}`, text, {
cfg: deps.cfg,
baseUrl: deps.baseUrl,
account: deps.account,
maxBytes: deps.mediaMaxBytes,
accountId: deps.accountId
});
},
log: logVerbose
})) return;
}
if (isGroup) {
if (senderAccess.decision !== "allow") {
if (senderAccess.reasonCode === "group_policy_disabled") logVerbose("Blocked signal group message (groupPolicy: disabled)");
else if (senderAccess.reasonCode === "group_policy_empty_allowlist") logVerbose("Blocked signal group message (groupPolicy: allowlist, no groupAllowFrom)");
else logVerbose(`Blocked signal group sender ${senderDisplay} (not in groupAllowFrom)`);
return;
}
}
const commandAuthorized = commandAccess.authorized;
if (isGroup && commandAccess.shouldBlockControlCommand) {
logInboundDrop({
log: logVerbose,
channel: "signal",
reason: "control command (unauthorized)",
target: senderDisplay
});
return;
}
const route = resolveSignalInboundRoute({
cfg: deps.cfg,
accountId: deps.accountId,
isGroup,
groupId,
senderPeerId
});
const mentionRegexes = buildMentionRegexes(deps.cfg, route.agentId);
const wasMentioned = isGroup && matchesMentionPatterns(messageText, mentionRegexes);
const requireMention = isGroup && resolveChannelGroupRequireMention({
cfg: deps.cfg,
channel: "signal",
groupId,
accountId: deps.accountId,
configuredGroupDefaultsToNoMention: true
});
const canDetectMention = mentionRegexes.length > 0;
const mentionDecision = resolveInboundMentionDecision({
facts: {
canDetectMention,
wasMentioned,
hasAnyMention: false,
implicitMentionKinds: []
},
policy: {
isGroup,
requireMention,
allowTextCommands: true,
hasControlCommand: hasControlCommandInMessage,
commandAuthorized
}
});
const effectiveWasMentioned = mentionDecision.effectiveWasMentioned;
if (isGroup && requireMention && canDetectMention && mentionDecision.shouldSkip) {
logInboundDrop({
log: logVerbose,
channel: "signal",
reason: "no mention",
target: senderDisplay
});
const pendingPlaceholder = (() => {
if (!dataMessage.attachments?.length) return "";
if (deps.ignoreAttachments) return "<media:attachment>";
const attachmentTypes = (dataMessage.attachments ?? []).map((attachment) => typeof attachment?.contentType === "string" ? attachment.contentType : void 0);
if (attachmentTypes.length > 1) return formatAttachmentSummaryPlaceholder(attachmentTypes);
const firstContentType = dataMessage.attachments?.[0]?.contentType;
const pendingKind = kindFromMime(firstContentType ?? void 0);
return pendingKind ? `<media:${pendingKind}>` : "<media:attachment>";
})();
const pendingBodyText = messageText || pendingPlaceholder || visibleQuoteText;
const historyKey = groupId ?? "unknown";
createChannelHistoryWindow({ historyMap: deps.groupHistories }).record({
historyKey,
limit: deps.historyLimit,
entry: {
sender: envelope.sourceName ?? senderDisplay,
body: pendingBodyText,
timestamp: envelope.timestamp ?? void 0,
messageId: typeof envelope.timestamp === "number" ? String(envelope.timestamp) : void 0
}
});
const signalGroupPolicy = resolveChannelGroupPolicy({
cfg: deps.cfg,
channel: "signal",
groupId,
accountId: deps.accountId
});
if ((signalGroupPolicy.groupConfig?.ingest ?? signalGroupPolicy.defaultConfig?.ingest) === true) {
const canonicalGroupTarget = normalizeSignalMessagingTarget(`group:${groupId}`) ?? `group:${groupId}`;
fireAndForgetHook(triggerInternalHook(createInternalHookEvent("message", "received", route.sessionKey, toInternalMessageReceivedContext({
from: `group:${groupId}`,
to: canonicalGroupTarget,
content: pendingBodyText,
timestamp: envelope.timestamp ?? void 0,
channelId: "signal",
accountId: deps.accountId,
conversationId: canonicalGroupTarget,
messageId: typeof envelope.timestamp === "number" ? String(envelope.timestamp) : void 0,
senderId: senderDisplay,
senderName: envelope.sourceName ?? void 0,
provider: "signal",
surface: "signal",
originatingChannel: "signal",
originatingTo: canonicalGroupTarget,
isGroup: true,
groupId: canonicalGroupTarget
}))), "signal: mention-skip message hook failed");
}
return;
}
let mediaPath;
let mediaType;
const mediaPaths = [];
const mediaTypes = [];
let placeholder = "";
const attachments = dataMessage.attachments ?? [];
if (!deps.ignoreAttachments) for (const attachment of attachments) {
if (!attachment?.id) continue;
try {
const fetched = await deps.fetchAttachment({
baseUrl: deps.baseUrl,
account: deps.account,
attachment,
sender: senderRecipient,
groupId,
maxBytes: deps.mediaMaxBytes
});
if (fetched) {
mediaPaths.push(fetched.path);
mediaTypes.push(fetched.contentType ?? attachment.contentType ?? "application/octet-stream");
if (!mediaPath) {
mediaPath = fetched.path;
mediaType = fetched.contentType ?? attachment.contentType ?? void 0;
}
}
} catch (err) {
deps.runtime.error?.(danger(`attachment fetch failed: ${String(err)}`));
}
}
if (mediaPaths.length > 1) placeholder = formatAttachmentSummaryPlaceholder(mediaTypes);
else {
const kind = kindFromMime(mediaType ?? void 0);
if (kind) placeholder = `<media:${kind}>`;
else if (attachments.length) placeholder = "<media:attachment>";
}
const bodyText = messageText || placeholder || visibleQuoteText || "";
if (!bodyText) return;
const receiptTimestamp = typeof envelope.timestamp === "number" ? envelope.timestamp : typeof dataMessage.timestamp === "number" ? dataMessage.timestamp : void 0;
if (deps.sendReadReceipts && !deps.readReceiptsViaDaemon && !isGroup && receiptTimestamp) try {
await sendReadReceiptSignal(`signal:${senderRecipient}`, receiptTimestamp, {
cfg: deps.cfg,
baseUrl: deps.baseUrl,
account: deps.account,
accountId: deps.accountId
});
} catch (err) {
logVerbose(`signal read receipt failed for ${senderDisplay}: ${String(err)}`);
}
else if (deps.sendReadReceipts && !deps.readReceiptsViaDaemon && !isGroup && !receiptTimestamp) logVerbose(`signal read receipt skipped (missing timestamp) for ${senderDisplay}`);
const senderName = envelope.sourceName ?? senderDisplay;
const messageId = typeof envelope.timestamp === "number" ? String(envelope.timestamp) : void 0;
await inboundDebouncer.enqueue({
senderName,
senderDisplay,
senderRecipient,
senderPeerId,
groupId,
groupName,
isGroup,
bodyText,
commandBody: messageText,
timestamp: envelope.timestamp ?? void 0,
messageId,
mediaPath,
mediaType,
mediaPaths: mediaPaths.length > 0 ? mediaPaths : void 0,
mediaTypes: mediaTypes.length > 0 ? mediaTypes : void 0,
commandAuthorized,
wasMentioned: effectiveWasMentioned,
replyToBody: visibleQuoteText || void 0,
replyToSender: visibleQuoteSender,
replyToIsQuote: visibleQuoteText ? true : void 0
});
};
}
//#endregion
//#region extensions/signal/src/sse-reconnect.ts
const DEFAULT_RECONNECT_POLICY = {
initialMs: 1e3,
maxMs: 1e4,
factor: 2,
jitter: .2
};
async function runSignalSseLoop({ baseUrl, account, abortSignal, runtime, onEvent, timeoutMs, apiMode, policy }) {
const reconnectPolicy = {
...DEFAULT_RECONNECT_POLICY,
...policy
};
let reconnectAttempts = 0;
const logReconnectVerbose = (message) => {
if (!shouldLogVerbose()) return;
logVerbose(message);
};
for (;;) {
if (abortSignal?.aborted) break;
try {
await streamSignalEvents({
baseUrl,
account,
abortSignal,
timeoutMs,
apiMode,
onEvent: (event) => {
reconnectAttempts = 0;
onEvent(event);
},
logger: {
log: runtime.log,
error: runtime.error
}
});
if (abortSignal?.aborted) return;
reconnectAttempts += 1;
const delayMs = computeBackoff(reconnectPolicy, reconnectAttempts);
logReconnectVerbose(`Signal stream ended, reconnecting in ${delayMs / 1e3}s...`);
await sleepWithAbort(delayMs, abortSignal);
} catch (err) {
if (abortSignal?.aborted) return;
runtime.error?.(`Signal stream error: ${String(err)}`);
reconnectAttempts += 1;
const delayMs = computeBackoff(reconnectPolicy, reconnectAttempts);
runtime.log?.(`Signal connection lost, reconnecting in ${delayMs / 1e3}s...`);
try {
await sleepWithAbort(delayMs, abortSignal);
} catch (sleepErr) {
if (abortSignal?.aborted) return;
throw sleepErr;
}
}
}
}
//#endregion
//#region extensions/signal/src/monitor.ts
function resolveRuntime(opts) {
return opts.runtime ?? createNonExitingRuntime();
}
function createSignalMonitorTaskRunner(runtime) {
const inFlight = /* @__PURE__ */ new Set();
return {
runEventTask(task) {
const trackedTask = Promise.resolve().then(task).catch((err) => runtime.error?.(`event handler failed: ${String(err)}`)).finally(() => inFlight.delete(trackedTask));
inFlight.add(trackedTask);
},
async waitForIdle() {
while (inFlight.size > 0) await Promise.allSettled(inFlight);
}
};
}
function mergeAbortSignals(a, b) {
if (!a && !b) return {
signal: void 0,
dispose: () => {}
};
if (!a) return {
signal: b,
dispose: () => {}
};
if (!b) return {
signal: a,
dispose: () => {}
};
const controller = new AbortController();
const abortFrom = (source) => {
if (!controller.signal.aborted) controller.abort(source.reason);
};
if (a.aborted) {
abortFrom(a);
return {
signal: controller.signal,
dispose: () => {}
};
}
if (b.aborted) {
abortFrom(b);
return {
signal: controller.signal,
dispose: () => {}
};
}
const onAbortA = () => abortFrom(a);
const onAbortB = () => abortFrom(b);
a.addEventListener("abort", onAbortA, { once: true });
b.addEventListener("abort", onAbortB, { once: true });
return {
signal: controller.signal,
dispose: () => {
a.removeEventListener("abort", onAbortA);
b.removeEventListener("abort", onAbortB);
}
};
}
function createSignalDaemonLifecycle(params) {
let daemonHandle = null;
let daemonStopRequested = false;
let daemonExitError;
const daemonAbortController = new AbortController();
const mergedAbort = mergeAbortSignals(params.abortSignal, daemonAbortController.signal);
const stop = () => {
daemonStopRequested = true;
daemonHandle?.stop();
};
const attach = (handle) => {
daemonHandle = handle;
handle.exited.then((exit) => {
if (daemonStopRequested || params.abortSignal?.aborted) return;
daemonExitError = new Error(formatSignalDaemonExit(exit));
if (!daemonAbortController.signal.aborted) daemonAbortController.abort(daemonExitError);
});
};
const getExitError = () => daemonExitError;
return {
attach,
stop,
getExitError,
abortSignal: mergedAbort.signal,
dispose: mergedAbort.dispose
};
}
function normalizeAllowList(raw) {
return normalizeStringEntries(raw);
}
function resolveSignalReactionTargets(reaction) {
const targets = [];
const uuid = reaction.targetAuthorUuid?.trim();
if (uuid) targets.push({
kind: "uuid",
id: uuid,
display: `uuid:${uuid}`
});
const author = reaction.targetAuthor?.trim();
if (author) {
const normalized = normalizeE164(author);
targets.push({
kind: "phone",
id: normalized,
display: normalized
});
}
return targets;
}
function isSignalReactionMessage(reaction) {
if (!reaction) return false;
const emoji = reaction.emoji?.trim();
const timestamp = reaction.targetSentTimestamp;
const hasTarget = Boolean(normalizeOptionalString(reaction.targetAuthor) || normalizeOptionalString(reaction.targetAuthorUuid));
return Boolean(emoji && typeof timestamp === "number" && timestamp > 0 && hasTarget);
}
function shouldEmitSignalReactionNotification(params) {
const { mode, account, targets, sender, allowlist } = params;
const effectiveMode = mode ?? "own";
if (effectiveMode === "off") return false;
if (effectiveMode === "own") {
const accountId = account?.trim();
if (!accountId || !targets || targets.length === 0) return false;
const normalizedAccount = normalizeE164(accountId);
return targets.some((target) => {
if (target.kind === "uuid") return accountId === target.id || accountId === `uuid:${target.id}`;
return normalizedAccount === target.id;
});
}
if (effectiveMode === "allowlist") {
if (!sender || !allowlist || allowlist.length === 0) return false;
return isSignalSenderAllowed(sender, allowlist);
}
return true;
}
function buildSignalReactionSystemEventText(params) {
const base = `Signal reaction added: ${params.emojiLabel} by ${params.actorLabel} msg ${params.messageId}`;
const withTarget = params.targetLabel ? `${base} from ${params.targetLabel}` : base;
return params.groupLabel ? `${withTarget} in ${params.groupLabel}` : withTarget;
}
async function waitForSignalDaemonReady(params) {
await (params.waitForTransportReadyFn ?? waitForTransportReady)({
label: "signal daemon",
timeoutMs: params.timeoutMs,
logAfterMs: params.logAfterMs,
logIntervalMs: params.logIntervalMs,
pollIntervalMs: 150,
abortSignal: params.abortSignal,
runtime: params.runtime,
check: async () => {
const res = await signalCheck(params.baseUrl, 1e3);
if (res.ok) return { ok: true };
return {
ok: false,
error: res.error ?? (res.status ? `HTTP ${res.status}` : "unreachable")
};
}
});
}
const SIGNAL_ATTACHMENT_RPC_RESPONSE_HEADROOM_BYTES = 64 * 1024;
const SIGNAL_BASE64_OVERHEAD_NUMERATOR = 4;
const SIGNAL_BASE64_OVERHEAD_DENOMINATOR = 3;
function deriveSignalAttachmentRpcMaxResponseBytes(maxBytes) {
if (!Number.isFinite(maxBytes) || maxBytes <= 0) return;
return Math.ceil(maxBytes * SIGNAL_BASE64_OVERHEAD_NUMERATOR / SIGNAL_BASE64_OVERHEAD_DENOMINATOR) + SIGNAL_ATTACHMENT_RPC_RESPONSE_HEADROOM_BYTES;
}
async function fetchAttachment(params) {
const { attachment } = params;
if (!attachment?.id) return null;
if (typeof attachment.size === "number" && attachment.size > params.maxBytes) throw new Error(`Signal attachment ${attachment.id} exceeds ${(params.maxBytes / (1024 * 1024)).toFixed(0)}MB limit`);
const rpcParams = { id: attachment.id };
if (params.account) rpcParams.account = params.account;
if (params.groupId) rpcParams.groupId = params.groupId;
else if (params.sender) rpcParams.recipient = params.sender;
else return null;
const result = await signalRpcRequest("getAttachment", rpcParams, {
baseUrl: params.baseUrl,
maxResponseBytes: deriveSignalAttachmentRpcMaxResponseBytes(params.maxBytes),
apiMode: params.apiMode
});
if (!result?.data) return null;
if (estimateBase64DecodedBytes(result.data) > params.maxBytes) throw new Error(`Signal attachment ${attachment.id} exceeds ${(params.maxBytes / (1024 * 1024)).toFixed(0)}MB limit`);
const buffer = Buffer.from(result.data, "base64");
const originalFilename = normalizeOptionalString(attachment.filename ?? void 0);
const saved = await saveMediaBuffer(buffer, normalizeOptionalString(attachment.contentType ?? void 0) ?? await detectMime({
buffer,
filePath: originalFilename
}), "inbound", params.maxBytes, originalFilename);
return {
path: saved.path,
contentType: saved.contentType
};
}
async function deliverReplies(params) {
const { replies, target, baseUrl, account, accountId, runtime, maxBytes, textLimit, chunkMode } = params;
for (const payload of replies) if (await deliverTextOrMediaReply({
payload,
text: resolveSendableOutboundReplyParts(payload).text,
chunkText: (value) => chunkTextWithMode(value, textLimit, chunkMode),
sendText: async (chunk) => {
await sendMessageSignal(target, chunk, {
cfg: params.cfg,
baseUrl,
account,
maxBytes,
accountId
});
},
sendMedia: async ({ mediaUrl, caption }) => {
await sendMessageSignal(target, caption ?? "", {
cfg: params.cfg,
baseUrl,
account,
mediaUrl,
maxBytes,
accountId
});
}
}) !== "empty") runtime.log?.(`delivered reply to ${target}`);
}
async function monitorSignalProvider(opts = {}) {
const runtime = resolveRuntime(opts);
const cfg = opts.config ?? getRuntimeConfig();
const accountInfo = resolveSignalAccount({
cfg,
accountId: opts.accountId
});
const historyLimit = Math.max(0, accountInfo.config.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? 50);
const groupHistories = /* @__PURE__ */ new Map();
const textLimit = resolveTextChunkLimit(cfg, "signal", accountInfo.accountId);
const chunkMode = resolveChunkMode(cfg, "signal", accountInfo.accountId);
const baseUrl = normalizeOptionalString(opts.baseUrl) ?? accountInfo.baseUrl;
const account = normalizeOptionalString(opts.account) ?? normalizeOptionalString(accountInfo.config.account);
const dmPolicy = accountInfo.config.dmPolicy ?? "pairing";
const allowFrom = normalizeAllowList(opts.allowFrom ?? accountInfo.config.allowFrom);
const groupAllowFrom = normalizeAllowList(opts.groupAllowFrom ?? accountInfo.config.groupAllowFrom ?? (accountInfo.config.allowFrom && accountInfo.config.allowFrom.length > 0 ? accountInfo.config.allowFrom : []));
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.signal !== void 0,
groupPolicy: accountInfo.config.groupPolicy,
defaultGroupPolicy
});
warnMissingProviderGroupPolicyFallbackOnce({
providerMissingFallbackApplied,
providerKey: "signal",
accountId: accountInfo.accountId,
log: (message) => runtime.log?.(message)
});
const reactionMode = accountInfo.config.reactionNotifications ?? "own";
const reactionAllowlist = normalizeAllowList(accountInfo.config.reactionAllowlist);
const mediaMaxBytes = (opts.mediaMaxMb ?? accountInfo.config.mediaMaxMb ?? 8) * 1024 * 1024;
const ignoreAttachments = opts.ignoreAttachments ?? accountInfo.config.ignoreAttachments ?? false;
const sendReadReceipts = Boolean(opts.sendReadReceipts ?? accountInfo.config.sendReadReceipts);
const waitForTransportReadyFn = opts.waitForTransportReady ?? waitForTransportReady;
const autoStart = opts.autoStart ?? accountInfo.config.autoStart ?? !accountInfo.config.httpUrl;
const configuredApiMode = cfg.channels?.signal?.apiMode ?? "auto";
const startupTimeoutMs = Math.min(12e4, Math.max(1e3, opts.startupTimeoutMs ?? accountInfo.config.startupTimeoutMs ?? 3e4));
const readReceiptsViaDaemon = autoStart && sendReadReceipts;
const daemonLifecycle = createSignalDaemonLifecycle({ abortSignal: opts.abortSignal });
const monitorTaskRunner = createSignalMonitorTaskRunner(runtime);
let daemonHandle = null;
if (autoStart && configuredApiMode === "container") throw new Error("channels.signal.autoStart=true is incompatible with channels.signal.apiMode=container");
if (autoStart) {
const cliPath = opts.cliPath ?? accountInfo.config.cliPath ?? "signal-cli";
const configPath = normalizeOptionalString(opts.configPath) ?? normalizeOptionalString(accountInfo.config.configPath);
const httpHost = opts.httpHost ?? accountInfo.config.httpHost ?? "127.0.0.1";
const httpPort = opts.httpPort ?? accountInfo.config.httpPort ?? 8080;
daemonHandle = spawnSignalDaemon({
cliPath,
...configPath ? { configPath } : {},
account,
httpHost,
httpPort,
receiveMode: opts.receiveMode ?? accountInfo.config.receiveMode,
ignoreAttachments: opts.ignoreAttachments ?? accountInfo.config.ignoreAttachments,
ignoreStories: opts.ignoreStories ?? accountInfo.config.ignoreStories,
sendReadReceipts,
runtime
});
daemonLifecycle.attach(daemonHandle);
}
const onAbort = () => {
daemonLifecycle.stop();
};
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
try {
if (daemonHandle) {
await waitForSignalDaemonReady({
baseUrl,
abortSignal: daemonLifecycle.abortSignal,
timeoutMs: startupTimeoutMs,
logAfterMs: 1e4,
logIntervalMs: 1e4,
runtime,
waitForTransportReadyFn
});
const daemonExitError = daemonLifecycle.getExitError();
if (daemonExitError) throw daemonExitError;
}
registerChannelRuntimeContext({
channelRuntime: opts.channelRuntime,
channelId: "signal",
accountId: accountInfo.accountId,
capability: CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY,
context: isSignalNativeApprovalHandlerConfigured({
cfg,
accountId: accountInfo.accountId
}) ? {
accountId: accountInfo.accountId,
baseUrl,
account,
accountUuid: accountInfo.config.accountUuid
} : null,
abortSignal: opts.abortSignal
});
const handleEvent = createSignalEventHandler({
runtime,
cfg,
baseUrl,
account,
accountUuid: accountInfo.config.accountUuid,
accountId: accountInfo.accountId,
blockStreaming: accountInfo.config.blockStreaming,
historyLimit,
groupHistories,
textLimit,
dmPolicy,
allowFrom,
groupAllowFrom,
groupPolicy,
reactionMode,
reactionAllowlist,
mediaMaxBytes,
ignoreAttachments,
sendReadReceipts,
readReceiptsViaDaemon,
fetchAttachment: (params) => fetchAttachment({
...params,
apiMode: configuredApiMode
}),
deliverReplies: (params) => deliverReplies({
...params,
cfg,
chunkMode
}),
resolveSignalReactionTargets,
isSignalReactionMessage,
shouldEmitSignalReactionNotification,
buildSignalReactionSystemEventText
});
await runSignalSseLoop({
baseUrl,
account,
abortSignal: daemonLifecycle.abortSignal,
runtime,
timeoutMs: 0,
apiMode: configuredApiMode,
policy: opts.reconnectPolicy,
onEvent: (event) => {
monitorTaskRunner.runEventTask(() => handleEvent(event));
}
});
const daemonExitError = daemonLifecycle.getExitError();
if (daemonExitError) throw daemonExitError;
} catch (err) {
const daemonExitError = daemonLifecycle.getExitError();
if (opts.abortSignal?.aborted && !daemonExitError) return;
throw err;
} finally {
await monitorTaskRunner.waitForIdle();
daemonLifecycle.dispose();
opts.abortSignal?.removeEventListener("abort", onAbort);
daemonLifecycle.stop();
}
}
//#endregion
export { monitorSignalProvider as t };