fix(logging): redact phone numbers and message content from WhatsApp logs

Apply redactIdentifier() (SHA-256 hashing) to all recipient JIDs and
phone numbers logged by sendMessageWhatsApp, sendReactionWhatsApp,
sendPollWhatsApp, and runWebHeartbeatOnce. Remove poll question text
and message preview content from log entries, replacing with character
counts where useful for debugging.

The existing redactIdentifier() utility in src/logging/redact-identifier.ts
was already implemented but not wired into any WhatsApp logging path.
This commit connects it to all affected call sites while leaving
functional parameters (actual send calls, event emitters) untouched.

Closes #24957
This commit is contained in:
Coy Geek
2026-02-23 18:38:40 -08:00
committed by Peter Steinberger
parent 0bdcca2f35
commit aef45b2abb
2 changed files with 51 additions and 40 deletions

View File

@@ -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<typeof loadConfig>;
@@ -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",

View File

@@ -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<void> {
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;
}
}