diff --git a/src/web/auto-reply/heartbeat-runner.ts b/src/web/auto-reply/heartbeat-runner.ts index 5b89c785c..e393339a7 100644 --- a/src/web/auto-reply/heartbeat-runner.ts +++ b/src/web/auto-reply/heartbeat-runner.ts @@ -18,13 +18,13 @@ import { import { emitHeartbeatEvent, resolveIndicatorType } from "../../infra/heartbeat-events.js"; import { resolveHeartbeatVisibility } from "../../infra/heartbeat-visibility.js"; import { getChildLogger } from "../../logging.js"; +import { redactIdentifier } from "../../logging/redact-identifier.js"; import { normalizeMainKey } from "../../routing/session-key.js"; import { sendMessageWhatsApp } from "../outbound.js"; import { newConnectionId } from "../reconnect.js"; import { formatError } from "../session.js"; import { whatsappHeartbeatLog } from "./loggers.js"; import { getSessionSnapshot } from "./session-snapshot.js"; -import { elide } from "./util.js"; export async function runWebHeartbeatOnce(opts: { cfg?: ReturnType; @@ -40,10 +40,11 @@ export async function runWebHeartbeatOnce(opts: { const replyResolver = opts.replyResolver ?? getReplyFromConfig; const sender = opts.sender ?? sendMessageWhatsApp; const runId = newConnectionId(); + const redactedTo = redactIdentifier(to); const heartbeatLogger = getChildLogger({ module: "web-heartbeat", runId, - to, + to: redactedTo, }); const cfg = cfgOverride ?? loadConfig(); @@ -57,20 +58,20 @@ export async function runWebHeartbeatOnce(opts: { return false; } if (dryRun) { - whatsappHeartbeatLog.info(`[dry-run] heartbeat ok -> ${to}`); + whatsappHeartbeatLog.info(`[dry-run] heartbeat ok -> ${redactedTo}`); return false; } const sendResult = await sender(to, heartbeatOkText, { verbose }); heartbeatLogger.info( { - to, + to: redactedTo, messageId: sendResult.messageId, chars: heartbeatOkText.length, reason: "heartbeat-ok", }, "heartbeat ok sent", ); - whatsappHeartbeatLog.info(`heartbeat ok sent to ${to} (id ${sendResult.messageId})`); + whatsappHeartbeatLog.info(`heartbeat ok sent to ${redactedTo} (id ${sendResult.messageId})`); return true; }; @@ -100,7 +101,7 @@ export async function runWebHeartbeatOnce(opts: { if (verbose) { heartbeatLogger.info( { - to, + to: redactedTo, sessionKey: sessionSnapshot.key, sessionId: sessionId ?? sessionSnapshot.entry?.sessionId ?? null, sessionFresh: sessionSnapshot.fresh, @@ -122,7 +123,7 @@ export async function runWebHeartbeatOnce(opts: { if (overrideBody) { if (dryRun) { whatsappHeartbeatLog.info( - `[dry-run] web send -> ${to}: ${elide(overrideBody.trim(), 200)} (manual message)`, + `[dry-run] web send -> ${redactedTo} (${overrideBody.trim().length} chars, manual message)`, ); return; } @@ -137,19 +138,21 @@ export async function runWebHeartbeatOnce(opts: { }); heartbeatLogger.info( { - to, + to: redactedTo, messageId: sendResult.messageId, chars: overrideBody.length, reason: "manual-message", }, "manual heartbeat message sent", ); - whatsappHeartbeatLog.info(`manual heartbeat sent to ${to} (id ${sendResult.messageId})`); + whatsappHeartbeatLog.info( + `manual heartbeat sent to ${redactedTo} (id ${sendResult.messageId})`, + ); return; } if (!visibility.showAlerts && !visibility.showOk && !visibility.useIndicator) { - heartbeatLogger.info({ to, reason: "alerts-disabled" }, "heartbeat skipped"); + heartbeatLogger.info({ to: redactedTo, reason: "alerts-disabled" }, "heartbeat skipped"); emitHeartbeatEvent({ status: "skipped", to, @@ -181,7 +184,7 @@ export async function runWebHeartbeatOnce(opts: { ) { heartbeatLogger.info( { - to, + to: redactedTo, reason: "empty-reply", sessionId: sessionSnapshot.entry?.sessionId ?? null, }, @@ -226,7 +229,7 @@ export async function runWebHeartbeatOnce(opts: { } heartbeatLogger.info( - { to, reason: "heartbeat-token", rawLength: replyPayload.text?.length }, + { to: redactedTo, reason: "heartbeat-token", rawLength: replyPayload.text?.length }, "heartbeat skipped", ); const okSent = await maybeSendHeartbeatOk(); @@ -241,14 +244,17 @@ export async function runWebHeartbeatOnce(opts: { } if (hasMedia) { - heartbeatLogger.warn({ to }, "heartbeat reply contained media; sending text only"); + heartbeatLogger.warn( + { to: redactedTo }, + "heartbeat reply contained media; sending text only", + ); } const finalText = stripped.text || replyPayload.text || ""; // Check if alerts are disabled for WhatsApp if (!visibility.showAlerts) { - heartbeatLogger.info({ to, reason: "alerts-disabled" }, "heartbeat skipped"); + heartbeatLogger.info({ to: redactedTo, reason: "alerts-disabled" }, "heartbeat skipped"); emitHeartbeatEvent({ status: "skipped", to, @@ -262,8 +268,11 @@ export async function runWebHeartbeatOnce(opts: { } if (dryRun) { - heartbeatLogger.info({ to, reason: "dry-run", chars: finalText.length }, "heartbeat dry-run"); - whatsappHeartbeatLog.info(`[dry-run] heartbeat -> ${to}: ${elide(finalText, 200)}`); + heartbeatLogger.info( + { to: redactedTo, reason: "dry-run", chars: finalText.length }, + "heartbeat dry-run", + ); + whatsappHeartbeatLog.info(`[dry-run] heartbeat -> ${redactedTo} (${finalText.length} chars)`); return; } @@ -278,17 +287,16 @@ export async function runWebHeartbeatOnce(opts: { }); heartbeatLogger.info( { - to, + to: redactedTo, messageId: sendResult.messageId, chars: finalText.length, - preview: elide(finalText, 140), }, "heartbeat sent", ); - whatsappHeartbeatLog.info(`heartbeat alert sent to ${to}`); + whatsappHeartbeatLog.info(`heartbeat alert sent to ${redactedTo}`); } catch (err) { const reason = formatError(err); - heartbeatLogger.warn({ to, error: reason }, "heartbeat failed"); + heartbeatLogger.warn({ to: redactedTo, error: reason }, "heartbeat failed"); whatsappHeartbeatLog.warn(`heartbeat failed (${reason})`); emitHeartbeatEvent({ status: "failed", diff --git a/src/web/outbound.ts b/src/web/outbound.ts index ce8b44669..da1428a69 100644 --- a/src/web/outbound.ts +++ b/src/web/outbound.ts @@ -2,6 +2,7 @@ import { loadConfig } from "../config/config.js"; import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; import { generateSecureUuid } from "../infra/secure-random.js"; import { getChildLogger } from "../logging/logger.js"; +import { redactIdentifier } from "../logging/redact-identifier.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { convertMarkdownTables } from "../markdown/tables.js"; import { markdownToWhatsApp } from "../markdown/whatsapp.js"; @@ -37,13 +38,15 @@ export async function sendMessageWhatsApp( }); text = convertMarkdownTables(text ?? "", tableMode); text = markdownToWhatsApp(text); + const redactedTo = redactIdentifier(to); const logger = getChildLogger({ module: "web-outbound", correlationId, - to, + to: redactedTo, }); try { const jid = toWhatsappJid(to); + const redactedJid = redactIdentifier(jid); let mediaBuffer: Buffer | undefined; let mediaType: string | undefined; let documentFileName: string | undefined; @@ -69,8 +72,8 @@ export async function sendMessageWhatsApp( documentFileName = media.fileName; } } - outboundLog.info(`Sending message -> ${jid}${options.mediaUrl ? " (media)" : ""}`); - logger.info({ jid, hasMedia: Boolean(options.mediaUrl) }, "sending message"); + outboundLog.info(`Sending message -> ${redactedJid}${options.mediaUrl ? " (media)" : ""}`); + logger.info({ jid: redactedJid, hasMedia: Boolean(options.mediaUrl) }, "sending message"); await active.sendComposingTo(to); const hasExplicitAccountId = Boolean(options.accountId?.trim()); const accountId = hasExplicitAccountId ? resolvedAccountId : undefined; @@ -88,13 +91,13 @@ export async function sendMessageWhatsApp( const messageId = (result as { messageId?: string })?.messageId ?? "unknown"; const durationMs = Date.now() - startedAt; outboundLog.info( - `Sent message ${messageId} -> ${jid}${options.mediaUrl ? " (media)" : ""} (${durationMs}ms)`, + `Sent message ${messageId} -> ${redactedJid}${options.mediaUrl ? " (media)" : ""} (${durationMs}ms)`, ); - logger.info({ jid, messageId }, "sent message"); + logger.info({ jid: redactedJid, messageId }, "sent message"); return { messageId, toJid: jid }; } catch (err) { logger.error( - { err: String(err), to, hasMedia: Boolean(options.mediaUrl) }, + { err: String(err), to: redactedTo, hasMedia: Boolean(options.mediaUrl) }, "failed to send via web session", ); throw err; @@ -114,16 +117,18 @@ export async function sendReactionWhatsApp( ): Promise { const correlationId = generateSecureUuid(); const { listener: active } = requireActiveWebListener(options.accountId); + const redactedChatJid = redactIdentifier(chatJid); const logger = getChildLogger({ module: "web-outbound", correlationId, - chatJid, + chatJid: redactedChatJid, messageId, }); try { const jid = toWhatsappJid(chatJid); + const redactedJid = redactIdentifier(jid); outboundLog.info(`Sending reaction "${emoji}" -> message ${messageId}`); - logger.info({ chatJid: jid, messageId, emoji }, "sending reaction"); + logger.info({ chatJid: redactedJid, messageId, emoji }, "sending reaction"); await active.sendReaction( chatJid, messageId, @@ -132,10 +137,10 @@ export async function sendReactionWhatsApp( options.participant, ); outboundLog.info(`Sent reaction "${emoji}" -> message ${messageId}`); - logger.info({ chatJid: jid, messageId, emoji }, "sent reaction"); + logger.info({ chatJid: redactedJid, messageId, emoji }, "sent reaction"); } catch (err) { logger.error( - { err: String(err), chatJid, messageId, emoji }, + { err: String(err), chatJid: redactedChatJid, messageId, emoji }, "failed to send reaction via web session", ); throw err; @@ -150,19 +155,20 @@ export async function sendPollWhatsApp( const correlationId = generateSecureUuid(); const startedAt = Date.now(); const { listener: active } = requireActiveWebListener(options.accountId); + const redactedTo = redactIdentifier(to); const logger = getChildLogger({ module: "web-outbound", correlationId, - to, + to: redactedTo, }); try { const jid = toWhatsappJid(to); + const redactedJid = redactIdentifier(jid); const normalized = normalizePollInput(poll, { maxOptions: 12 }); - outboundLog.info(`Sending poll -> ${jid}: "${normalized.question}"`); + outboundLog.info(`Sending poll -> ${redactedJid}`); logger.info( { - jid, - question: normalized.question, + jid: redactedJid, optionCount: normalized.options.length, maxSelections: normalized.maxSelections, }, @@ -171,14 +177,11 @@ export async function sendPollWhatsApp( const result = await active.sendPoll(to, normalized); const messageId = (result as { messageId?: string })?.messageId ?? "unknown"; const durationMs = Date.now() - startedAt; - outboundLog.info(`Sent poll ${messageId} -> ${jid} (${durationMs}ms)`); - logger.info({ jid, messageId }, "sent poll"); + outboundLog.info(`Sent poll ${messageId} -> ${redactedJid} (${durationMs}ms)`); + logger.info({ jid: redactedJid, messageId }, "sent poll"); return { messageId, toJid: jid }; } catch (err) { - logger.error( - { err: String(err), to, question: poll.question }, - "failed to send poll via web session", - ); + logger.error({ err: String(err), to: redactedTo }, "failed to send poll via web session"); throw err; } }