diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index 9d1fe12e0..7ea457ac5 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -2,12 +2,7 @@ import crypto from "node:crypto"; import path from "node:path"; import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message.js"; -import { CLAUDE_BIN, CLAUDE_IDENTITY_PREFIX, parseClaudeJson } from "./claude.js"; -import { - applyTemplate, - type MsgContext, - type TemplateContext, -} from "./templating.js"; +import { loadConfig, type WarelayConfig } from "../config/config.js"; import { DEFAULT_IDLE_MINUTES, DEFAULT_RESET_TRIGGER, @@ -16,16 +11,25 @@ import { resolveStorePath, saveSessionStore, } from "../config/sessions.js"; -import { loadConfig, type WarelayConfig } from "../config/config.js"; import { info, isVerbose, logVerbose } from "../globals.js"; -import { enqueueCommand } from "../process/command-queue.js"; -import { runCommandWithTimeout } from "../process/exec.js"; -import { sendTypingIndicator } from "../twilio/typing.js"; -import type { TwilioRequester } from "../twilio/types.js"; -import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { logError } from "../logger.js"; import { ensureMediaHosted } from "../media/host.js"; -import { normalizeMediaSource, splitMediaFromOutput } from "../media/parse.js"; +import { splitMediaFromOutput } from "../media/parse.js"; +import { enqueueCommand } from "../process/command-queue.js"; +import { runCommandWithTimeout } from "../process/exec.js"; +import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import type { TwilioRequester } from "../twilio/types.js"; +import { sendTypingIndicator } from "../twilio/typing.js"; +import { + CLAUDE_BIN, + CLAUDE_IDENTITY_PREFIX, + parseClaudeJson, +} from "./claude.js"; +import { + applyTemplate, + type MsgContext, + type TemplateContext, +} from "./templating.js"; type GetReplyOptions = { onReplyStart?: () => Promise | void; @@ -51,20 +55,20 @@ function summarizeClaudeMetadata(payload: unknown): string | undefined { const usage = obj.usage; if (usage && typeof usage === "object") { - const serverToolUse = ( - usage as { server_tool_use?: Record } - ).server_tool_use; - if (serverToolUse && typeof serverToolUse === "object") { - const toolCalls = Object.values(serverToolUse).reduce( - (sum, val) => { - if (typeof val === "number") return sum + val; - return sum; - }, - 0, - ); - if (toolCalls > 0) parts.push(`tool_calls=${toolCalls}`); - } + const serverToolUse = ( + usage as { server_tool_use?: Record } + ).server_tool_use; + if (serverToolUse && typeof serverToolUse === "object") { + const toolCalls = Object.values(serverToolUse).reduce( + (sum, val) => { + if (typeof val === "number") return sum + val; + return sum; + }, + 0, + ); + if (toolCalls > 0) parts.push(`tool_calls=${toolCalls}`); } + } const modelUsage = obj.modelUsage; if (modelUsage && typeof modelUsage === "object") { @@ -168,8 +172,7 @@ export async function getReplyFromConfig( const prefixedBody = bodyPrefix ? `${bodyPrefix}${sessionCtx.BodyStripped ?? sessionCtx.Body ?? ""}` : (sessionCtx.BodyStripped ?? sessionCtx.Body); -const mediaNote = - ctx.MediaPath && ctx.MediaPath.length + const mediaNote = ctx.MediaPath?.length ? `[media attached: ${ctx.MediaPath}${ctx.MediaType ? ` (${ctx.MediaType})` : ""}${ctx.MediaUrl ? ` | ${ctx.MediaUrl}` : ""}]` : undefined; // For command prompts we prepend the media note so Claude et al. see it; text replies stay clean. @@ -208,7 +211,10 @@ const mediaNote = if (reply.mode === "text" && reply.text) { await onReplyStart(); logVerbose("Using text auto-reply from config"); - return { text: applyTemplate(reply.text, templatingCtx), mediaUrl: reply.mediaUrl }; + return { + text: applyTemplate(reply.text, templatingCtx), + mediaUrl: reply.mediaUrl, + }; } if (reply.mode === "command" && reply.command?.length) { @@ -303,7 +309,10 @@ const mediaNote = logVerbose(`Command auto-reply stderr: ${stderr.trim()}`); } let parsed: ClaudeJsonParseResult | undefined; - if (trimmed && (reply.claudeOutputFormat === "json" || isClaudeInvocation)) { + if ( + trimmed && + (reply.claudeOutputFormat === "json" || isClaudeInvocation) + ) { // Claude JSON mode: extract the human text for both logging and reply while keeping metadata. parsed = parseClaudeJson(trimmed); if (parsed?.parsed && isVerbose()) { @@ -333,7 +342,9 @@ const mediaNote = logVerbose("No MEDIA token extracted from final text"); } if (!trimmed && !mediaFromCommand) { - const meta = parsed ? summarizeClaudeMetadata(parsed.parsed) : undefined; + const meta = parsed + ? summarizeClaudeMetadata(parsed.parsed) + : undefined; trimmed = `(command produced no output${meta ? `; ${meta}` : ""})`; logVerbose("No text/media produced; injecting fallback notice to user"); } @@ -373,7 +384,9 @@ const mediaNote = `Command auto-reply timed out after ${elapsed}ms (limit ${timeoutMs}ms)`, ); } else { - logError(`Command auto-reply failed after ${elapsed}ms: ${String(err)}`); + logError( + `Command auto-reply failed after ${elapsed}ms: ${String(err)}`, + ); } return undefined; } @@ -431,7 +444,9 @@ export async function autoReplyIfConfigured( `Auto-replying via Twilio: from ${replyFrom} to ${replyTo}, body length ${replyResult.text.length}`, ); } else { - logVerbose(`Auto-replying via Twilio: from ${replyFrom} to ${replyTo} (media)`); + logVerbose( + `Auto-replying via Twilio: from ${replyFrom} to ${replyTo} (media)`, + ); } try { diff --git a/src/cli/deps.ts b/src/cli/deps.ts index bc18f4f9e..eded217da 100644 --- a/src/cli/deps.ts +++ b/src/cli/deps.ts @@ -1,21 +1,25 @@ +import { autoReplyIfConfigured } from "../auto-reply/reply.js"; +import { readEnv } from "../env.js"; +import { info } from "../globals.js"; import { ensureBinary } from "../infra/binaries.js"; import { ensurePortAvailable, handlePortError } from "../infra/ports.js"; import { ensureFunnel, getTailnetHostname } from "../infra/tailscale.js"; -import { waitForever } from "./wait.js"; -import { readEnv } from "../env.js"; -import { monitorTwilio as monitorTwilioImpl } from "../twilio/monitor.js"; -import { sendMessage, waitForFinalStatus } from "../twilio/send.js"; -import { sendMessageWeb, monitorWebProvider, logWebSelfId } from "../providers/web/index.js"; -import { assertProvider, sleep } from "../utils.js"; +import { ensureMediaHosted } from "../media/host.js"; +import { + logWebSelfId, + monitorWebProvider, + sendMessageWeb, +} from "../providers/web/index.js"; +import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { createClient } from "../twilio/client.js"; import { listRecentMessages } from "../twilio/messages.js"; -import { updateWebhook } from "../webhook/update.js"; +import { monitorTwilio as monitorTwilioImpl } from "../twilio/monitor.js"; +import { sendMessage, waitForFinalStatus } from "../twilio/send.js"; import { findWhatsappSenderSid } from "../twilio/senders.js"; +import { assertProvider, sleep } from "../utils.js"; import { startWebhook } from "../webhook/server.js"; -import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; -import { info } from "../globals.js"; -import { autoReplyIfConfigured } from "../auto-reply/reply.js"; -import { ensureMediaHosted } from "../media/host.js"; +import { updateWebhook } from "../webhook/update.js"; +import { waitForever } from "./wait.js"; export type CliDeps = { sendMessage: typeof sendMessage; diff --git a/src/cli/program.ts b/src/cli/program.ts index 87605d7c5..f0a417573 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -1,16 +1,19 @@ import { Command } from "commander"; - -import { setVerbose, setYes, danger, info, warn } from "../globals.js"; -import { defaultRuntime } from "../runtime.js"; import { sendCommand } from "../commands/send.js"; import { statusCommand } from "../commands/status.js"; import { upCommand } from "../commands/up.js"; import { webhookCommand } from "../commands/webhook.js"; -import { loginWeb, monitorWebProvider } from "../provider-web.js"; -import { pickProvider } from "../provider-web.js"; -import type { Provider } from "../utils.js"; -import { createDefaultDeps, logWebSelfId, logTwilioFrom, monitorTwilio } from "./deps.js"; import { ensureTwilioEnv } from "../env.js"; +import { danger, info, setVerbose, setYes, warn } from "../globals.js"; +import { loginWeb, monitorWebProvider, pickProvider } from "../provider-web.js"; +import { defaultRuntime } from "../runtime.js"; +import type { Provider } from "../utils.js"; +import { + createDefaultDeps, + logTwilioFrom, + logWebSelfId, + monitorTwilio, +} from "./deps.js"; import { spawnRelayTmux } from "./relay_tmux.js"; export function buildProgram() { @@ -50,20 +53,31 @@ export function buildProgram() { }); program - .command("send") - .description("Send a WhatsApp message") - .requiredOption( - "-t, --to ", - "Recipient number in E.164 (e.g. +15551234567)", - ) - .requiredOption("-m, --message ", "Message body") - .option("--media ", "Attach image (<=5MB). Web: path or URL. Twilio: https URL or local path hosted via webhook/funnel.") - .option("--serve-media", "For Twilio: start a temporary media server if webhook is not running", false) - .option("-w, --wait ", "Wait for delivery status (0 to skip)", "20") - .option("-p, --poll ", "Polling interval while waiting", "2") - .option("--provider ", "Provider: twilio | web", "twilio") - .option("--dry-run", "Print payload and skip sending", false) - .option("--json", "Output result as JSON", false) + .command("send") + .description("Send a WhatsApp message") + .requiredOption( + "-t, --to ", + "Recipient number in E.164 (e.g. +15551234567)", + ) + .requiredOption("-m, --message ", "Message body") + .option( + "--media ", + "Attach image (<=5MB). Web: path or URL. Twilio: https URL or local path hosted via webhook/funnel.", + ) + .option( + "--serve-media", + "For Twilio: start a temporary media server if webhook is not running", + false, + ) + .option( + "-w, --wait ", + "Wait for delivery status (0 to skip)", + "20", + ) + .option("-p, --poll ", "Polling interval while waiting", "2") + .option("--provider ", "Provider: twilio | web", "twilio") + .option("--dry-run", "Print payload and skip sending", false) + .option("--json", "Output result as JSON", false) .addHelpText( "after", ` @@ -201,7 +215,9 @@ With Tailscale: try { const server = await webhookCommand(opts, deps, defaultRuntime); if (!server) { - defaultRuntime.log(info("Webhook dry-run complete; no server started.")); + defaultRuntime.log( + info("Webhook dry-run complete; no server started."), + ); return; } process.on("SIGINT", () => { @@ -226,7 +242,11 @@ With Tailscale: .option("--path ", "Webhook path", "/webhook/whatsapp") .option("--verbose", "Verbose logging during setup/webhook", false) .option("-y, --yes", "Auto-confirm prompts when possible", false) - .option("--dry-run", "Print planned actions without touching network", false) + .option( + "--dry-run", + "Print planned actions without touching network", + false, + ) // istanbul ignore next .action(async (opts) => { setVerbose(Boolean(opts.verbose)); @@ -258,27 +278,36 @@ With Tailscale: ) .action(async () => { try { - const session = await spawnRelayTmux("pnpm warelay relay --verbose", true); + const session = await spawnRelayTmux( + "pnpm warelay relay --verbose", + true, + ); defaultRuntime.log( info( `tmux session started and attached: ${session} (pane running "pnpm warelay relay --verbose")`, ), ); } catch (err) { - defaultRuntime.error(danger(`Failed to start relay tmux session: ${String(err)}`)); + defaultRuntime.error( + danger(`Failed to start relay tmux session: ${String(err)}`), + ); defaultRuntime.exit(1); } }); program .command("relay:tmux:attach") - .description("Attach to the existing warelay-relay tmux session (no restart)") + .description( + "Attach to the existing warelay-relay tmux session (no restart)", + ) .action(async () => { try { await spawnRelayTmux("pnpm warelay relay --verbose", true, false); defaultRuntime.log(info("Attached to warelay-relay session.")); } catch (err) { - defaultRuntime.error(danger(`Failed to attach to warelay-relay: ${String(err)}`)); + defaultRuntime.error( + danger(`Failed to attach to warelay-relay: ${String(err)}`), + ); defaultRuntime.exit(1); } }); diff --git a/src/cli/prompt.ts b/src/cli/prompt.ts index 4036fc41e..63f778a9e 100644 --- a/src/cli/prompt.ts +++ b/src/cli/prompt.ts @@ -1,5 +1,5 @@ -import readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; +import readline from "node:readline/promises"; import { isVerbose, isYes } from "../globals.js"; diff --git a/src/commands/send.ts b/src/commands/send.ts index ab370891e..6c39e71c0 100644 --- a/src/commands/send.ts +++ b/src/commands/send.ts @@ -1,7 +1,7 @@ -import { info } from "../globals.js"; import type { CliDeps } from "../cli/deps.js"; -import type { Provider } from "../utils.js"; +import { info } from "../globals.js"; import type { RuntimeEnv } from "../runtime.js"; +import type { Provider } from "../utils.js"; export async function sendCommand( opts: { @@ -40,14 +40,10 @@ export async function sendCommand( runtime.log(info("Wait/poll are Twilio-only; ignored for provider=web.")); } const res = await deps - .sendMessageWeb( - opts.to, - opts.message, - { - verbose: false, - mediaUrl: opts.media, - }, - ) + .sendMessageWeb(opts.to, opts.message, { + verbose: false, + mediaUrl: opts.media, + }) .catch((err) => { runtime.error(`❌ Web send failed: ${String(err)}`); throw err; @@ -76,7 +72,7 @@ export async function sendCommand( return; } - let mediaUrl: string | undefined = undefined; + let mediaUrl: string | undefined; if (opts.media) { mediaUrl = await deps.resolveTwilioMediaUrl(opts.media, { serveMedia: Boolean(opts.serveMedia), diff --git a/src/commands/up.ts b/src/commands/up.ts index ef31361cc..6466a0b22 100644 --- a/src/commands/up.ts +++ b/src/commands/up.ts @@ -1,10 +1,16 @@ import type { CliDeps } from "../cli/deps.js"; -import type { RuntimeEnv } from "../runtime.js"; import { waitForever as defaultWaitForever } from "../cli/wait.js"; import { retryAsync } from "../infra/retry.js"; +import type { RuntimeEnv } from "../runtime.js"; export async function upCommand( - opts: { port: string; path: string; verbose?: boolean; yes?: boolean; dryRun?: boolean }, + opts: { + port: string; + path: string; + verbose?: boolean; + yes?: boolean; + dryRun?: boolean; + }, deps: CliDeps, runtime: RuntimeEnv, waiter: typeof defaultWaitForever = defaultWaitForever, diff --git a/src/commands/webhook.ts b/src/commands/webhook.ts index 42425122e..1df0f0262 100644 --- a/src/commands/webhook.ts +++ b/src/commands/webhook.ts @@ -1,6 +1,6 @@ import type { CliDeps } from "../cli/deps.js"; -import type { RuntimeEnv } from "../runtime.js"; import { retryAsync } from "../infra/retry.js"; +import type { RuntimeEnv } from "../runtime.js"; export async function webhookCommand( opts: { @@ -19,7 +19,9 @@ export async function webhookCommand( } await deps.ensurePortAvailable(port); if (opts.reply === "dry-run") { - runtime.log(`[dry-run] would start webhook on port ${port} path ${opts.path}`); + runtime.log( + `[dry-run] would start webhook on port ${port} path ${opts.path}`, + ); return undefined; } const server = await retryAsync( diff --git a/src/config/sessions.ts b/src/config/sessions.ts index 5bd53e5ef..261d34fc6 100644 --- a/src/config/sessions.ts +++ b/src/config/sessions.ts @@ -3,9 +3,8 @@ import os from "node:os"; import path from "node:path"; import JSON5 from "json5"; - -import { CONFIG_DIR, normalizeE164 } from "../utils.js"; import type { MsgContext } from "../auto-reply/templating.js"; +import { CONFIG_DIR, normalizeE164 } from "../utils.js"; export type SessionScope = "per-sender" | "global"; @@ -22,7 +21,9 @@ export function resolveStorePath(store?: string) { return path.resolve(store); } -export function loadSessionStore(storePath: string): Record { +export function loadSessionStore( + storePath: string, +): Record { try { const raw = fs.readFileSync(storePath, "utf-8"); const parsed = JSON5.parse(raw); diff --git a/src/index.ts b/src/index.ts index 7379eea8e..0f355f93c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,66 +3,56 @@ import process from "node:process"; import { fileURLToPath } from "node:url"; import dotenv from "dotenv"; -import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message.js"; -import type { TwilioRequester } from "./twilio/types.js"; -import { runCommandWithTimeout, runExec } from "./process/exec.js"; -import { sendTypingIndicator } from "./twilio/typing.js"; -import { autoReplyIfConfigured, getReplyFromConfig } from "./auto-reply/reply.js"; -import { readEnv, ensureTwilioEnv, type EnvConfig } from "./env.js"; -import { createClient } from "./twilio/client.js"; -import { logTwilioSendError, formatTwilioError } from "./twilio/utils.js"; -import { sendMessage, waitForFinalStatus } from "./twilio/send.js"; -import { startWebhook as startWebhookImpl } from "./twilio/webhook.js"; import { - updateWebhook as updateWebhookImpl, - findIncomingNumberSid as findIncomingNumberSidImpl, - findMessagingServiceSid as findMessagingServiceSidImpl, - setMessagingServiceWebhook as setMessagingServiceWebhookImpl, -} from "./twilio/update-webhook.js"; -import { listRecentMessages, formatMessageLine, uniqueBySid, sortByDateDesc } from "./twilio/messages.js"; -import { CLAUDE_BIN } from "./auto-reply/claude.js"; -import { applyTemplate, type MsgContext, type TemplateContext } from "./auto-reply/templating.js"; + autoReplyIfConfigured, + getReplyFromConfig, +} from "./auto-reply/reply.js"; +import { applyTemplate } from "./auto-reply/templating.js"; +import { createDefaultDeps, monitorTwilio } from "./cli/deps.js"; +import { promptYesNo } from "./cli/prompt.js"; +import { waitForever } from "./cli/wait.js"; +import { loadConfig } from "./config/config.js"; import { - CONFIG_PATH, - type WarelayConfig, - type SessionConfig, - type SessionScope, - type ReplyMode, - type ClaudeOutputFormat, - loadConfig, -} from "./config/config.js"; -import { sendCommand } from "./commands/send.js"; -import { statusCommand } from "./commands/status.js"; -import { upCommand } from "./commands/up.js"; -import { webhookCommand } from "./commands/webhook.js"; -import type { Provider } from "./utils.js"; -import { - assertProvider, - CONFIG_DIR, - jidToE164, - normalizeE164, - normalizePath, - sleep, - toWhatsappJid, - withWhatsAppPrefix, -} from "./utils.js"; -import { - DEFAULT_IDLE_MINUTES, - DEFAULT_RESET_TRIGGER, deriveSessionKey, loadSessionStore, resolveStorePath, saveSessionStore, - SESSION_STORE_DEFAULT, } from "./config/sessions.js"; -import { ensurePortAvailable, describePortOwner, PortInUseError, handlePortError } from "./infra/ports.js"; +import { readEnv } from "./env.js"; import { ensureBinary } from "./infra/binaries.js"; -import { ensureFunnel, ensureGoInstalled, ensureTailscaledInstalled, getTailnetHostname } from "./infra/tailscale.js"; -import { promptYesNo } from "./cli/prompt.js"; -import { waitForever } from "./cli/wait.js"; -import { findWhatsappSenderSid } from "./twilio/senders.js"; -import { createDefaultDeps, logTwilioFrom, logWebSelfId, monitorTwilio } from "./cli/deps.js"; +import { + describePortOwner, + ensurePortAvailable, + handlePortError, + PortInUseError, +} from "./infra/ports.js"; +import { + ensureFunnel, + ensureGoInstalled, + ensureTailscaledInstalled, + getTailnetHostname, +} from "./infra/tailscale.js"; +import { runCommandWithTimeout, runExec } from "./process/exec.js"; import { monitorWebProvider } from "./provider-web.js"; +import { createClient } from "./twilio/client.js"; +import { + formatMessageLine, + listRecentMessages, + sortByDateDesc, + uniqueBySid, +} from "./twilio/messages.js"; +import { sendMessage, waitForFinalStatus } from "./twilio/send.js"; +import { findWhatsappSenderSid } from "./twilio/senders.js"; +import { sendTypingIndicator } from "./twilio/typing.js"; +import { + findIncomingNumberSid as findIncomingNumberSidImpl, + findMessagingServiceSid as findMessagingServiceSidImpl, + setMessagingServiceWebhook as setMessagingServiceWebhookImpl, + updateWebhook as updateWebhookImpl, +} from "./twilio/update-webhook.js"; +import { formatTwilioError, logTwilioSendError } from "./twilio/utils.js"; +import { startWebhook as startWebhookImpl } from "./twilio/webhook.js"; +import { assertProvider, normalizeE164, toWhatsappJid } from "./utils.js"; dotenv.config({ quiet: true }); diff --git a/src/infra/ports.ts b/src/infra/ports.ts index 3b8fea2bd..458653e01 100644 --- a/src/infra/ports.ts +++ b/src/infra/ports.ts @@ -1,9 +1,8 @@ import net from "node:net"; - -import { runExec } from "../process/exec.js"; -import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { danger, info, isVerbose, logVerbose, warn } from "../globals.js"; import { logDebug } from "../logger.js"; +import { runExec } from "../process/exec.js"; +import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; class PortInUseError extends Error { port: number; @@ -21,7 +20,9 @@ function isErrno(err: unknown): err is NodeJS.ErrnoException { return Boolean(err && typeof err === "object" && "code" in err); } -export async function describePortOwner(port: number): Promise { +export async function describePortOwner( + port: number, +): Promise { // Best-effort process info for a listening port (macOS/Linux). try { const { stdout } = await runExec("lsof", [ diff --git a/src/infra/tailscale.ts b/src/infra/tailscale.ts index 3a44a6833..c3d7e94db 100644 --- a/src/infra/tailscale.ts +++ b/src/infra/tailscale.ts @@ -1,7 +1,6 @@ import chalk from "chalk"; - -import { danger, info, isVerbose, logVerbose, warn } from "../globals.js"; import { promptYesNo } from "../cli/prompt.js"; +import { danger, info, isVerbose, logVerbose, warn } from "../globals.js"; import { runExec } from "../process/exec.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { ensureBinary } from "./binaries.js"; diff --git a/src/logger.ts b/src/logger.ts index a7e03c8fa..4c06bd25e 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,4 +1,11 @@ -import { danger, info, success, warn, logVerbose, isVerbose } from "./globals.js"; +import { + danger, + info, + isVerbose, + logVerbose, + success, + warn, +} from "./globals.js"; import { defaultRuntime, type RuntimeEnv } from "./runtime.js"; export function logInfo(message: string, runtime: RuntimeEnv = defaultRuntime) { @@ -16,7 +23,10 @@ export function logSuccess( runtime.log(success(message)); } -export function logError(message: string, runtime: RuntimeEnv = defaultRuntime) { +export function logError( + message: string, + runtime: RuntimeEnv = defaultRuntime, +) { runtime.error(danger(message)); } diff --git a/src/media/host.ts b/src/media/host.ts index dfc161d64..bb7a1e3bd 100644 --- a/src/media/host.ts +++ b/src/media/host.ts @@ -1,13 +1,10 @@ -import { once } from "node:events"; import fs from "node:fs/promises"; -import path from "node:path"; -import { danger, warn } from "../globals.js"; -import { logInfo, logWarn } from "../logger.js"; -import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { ensurePortAvailable, PortInUseError } from "../infra/ports.js"; import { getTailnetHostname } from "../infra/tailscale.js"; -import { saveMediaSource } from "./store.js"; +import { logInfo } from "../logger.js"; +import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { startMediaServer } from "./server.js"; +import { saveMediaSource } from "./store.js"; const DEFAULT_PORT = 42873; const TTL_MS = 2 * 60 * 1000; diff --git a/src/media/parse.ts b/src/media/parse.ts index e522cb0ae..abb31408b 100644 --- a/src/media/parse.ts +++ b/src/media/parse.ts @@ -15,7 +15,11 @@ function isValidMedia(candidate: string) { if (!candidate) return false; if (candidate.length > 1024) return false; if (/\s/.test(candidate)) return false; - return /^https?:\/\//i.test(candidate) || candidate.startsWith("/") || candidate.startsWith("./"); + return ( + /^https?:\/\//i.test(candidate) || + candidate.startsWith("/") || + candidate.startsWith("./") + ); } export function splitMediaFromOutput(raw: string): { @@ -29,22 +33,21 @@ export function splitMediaFromOutput(raw: string): { const candidate = normalizeMediaSource(cleanCandidate(match[1])); const mediaUrl = isValidMedia(candidate) ? candidate : undefined; - const cleanedText = - mediaUrl - ? trimmedRaw - .replace(match[0], "") - .replace(/[ \t]+\n/g, "\n") - .replace(/[ \t]{2,}/g, " ") - .replace(/\n{2,}/g, "\n") - .trim() - : trimmedRaw - .split("\n") - .filter((line) => !MEDIA_TOKEN_RE.test(line)) - .join("\n") - .replace(/[ \t]+\n/g, "\n") - .replace(/[ \t]{2,}/g, " ") - .replace(/\n{2,}/g, "\n") - .trim(); + const cleanedText = mediaUrl + ? trimmedRaw + .replace(match[0], "") + .replace(/[ \t]+\n/g, "\n") + .replace(/[ \t]{2,}/g, " ") + .replace(/\n{2,}/g, "\n") + .trim() + : trimmedRaw + .split("\n") + .filter((line) => !MEDIA_TOKEN_RE.test(line)) + .join("\n") + .replace(/[ \t]+\n/g, "\n") + .replace(/[ \t]{2,}/g, " ") + .replace(/\n{2,}/g, "\n") + .trim(); return mediaUrl ? { text: cleanedText, mediaUrl } : { text: cleanedText }; } diff --git a/src/media/server.ts b/src/media/server.ts index 4af87a8d5..bd813be80 100644 --- a/src/media/server.ts +++ b/src/media/server.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; +import type { Server } from "node:http"; import path from "node:path"; import express, { type Express } from "express"; -import type { Server } from "http"; import { danger } from "../globals.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { cleanOldMedia, getMediaDir } from "./store.js"; @@ -11,7 +11,7 @@ const DEFAULT_TTL_MS = 2 * 60 * 1000; export function attachMediaRoutes( app: Express, ttlMs = DEFAULT_TTL_MS, - runtime: RuntimeEnv = defaultRuntime, + _runtime: RuntimeEnv = defaultRuntime, ) { const mediaDir = getMediaDir(); @@ -41,7 +41,6 @@ export function attachMediaRoutes( setInterval(() => { void cleanOldMedia(ttlMs); }, ttlMs).unref(); - } export async function startMediaServer( diff --git a/src/media/store.ts b/src/media/store.ts index 601ec7dff..1a6bc67e4 100644 --- a/src/media/store.ts +++ b/src/media/store.ts @@ -1,10 +1,10 @@ import crypto from "node:crypto"; -import fs from "node:fs/promises"; -import path from "node:path"; -import os from "node:os"; -import { pipeline } from "node:stream/promises"; import { createWriteStream } from "node:fs"; +import fs from "node:fs/promises"; import { request } from "node:https"; +import os from "node:os"; +import path from "node:path"; +import { pipeline } from "node:stream/promises"; const MEDIA_DIR = path.join(os.homedir(), ".warelay", "media"); const MAX_BYTES = 5 * 1024 * 1024; // 5MB @@ -58,7 +58,9 @@ async function downloadToFile( req.destroy(new Error("Media exceeds 5MB limit")); } }); - pipeline(res, out).then(() => resolve()).catch(reject); + pipeline(res, out) + .then(() => resolve()) + .catch(reject); }); req.on("error", reject); req.end(); diff --git a/src/providers/twilio/index.ts b/src/providers/twilio/index.ts index c89c233e4..de6089aa8 100644 --- a/src/providers/twilio/index.ts +++ b/src/providers/twilio/index.ts @@ -1,13 +1,16 @@ -export { sendTypingIndicator } from "../../twilio/typing.js"; export { createClient } from "../../twilio/client.js"; +export { + formatMessageLine, + listRecentMessages, +} from "../../twilio/messages.js"; export { monitorTwilio } from "../../twilio/monitor.js"; export { sendMessage, waitForFinalStatus } from "../../twilio/send.js"; -export { listRecentMessages, formatMessageLine } from "../../twilio/messages.js"; +export { findWhatsappSenderSid } from "../../twilio/senders.js"; +export { sendTypingIndicator } from "../../twilio/typing.js"; export { - updateWebhook, findIncomingNumberSid, findMessagingServiceSid, setMessagingServiceWebhook, + updateWebhook, } from "../../twilio/update-webhook.js"; -export { findWhatsappSenderSid } from "../../twilio/senders.js"; export { formatTwilioError, logTwilioSendError } from "../../twilio/utils.js"; diff --git a/src/providers/web/index.ts b/src/providers/web/index.ts index 44aaba6d2..873ae6fdb 100644 --- a/src/providers/web/index.ts +++ b/src/providers/web/index.ts @@ -1,12 +1,12 @@ export { createWaSocket, - waitForWaConnection, - sendMessageWeb, loginWeb, + logWebSelfId, monitorWebInbox, monitorWebProvider, - webAuthExists, - logWebSelfId, pickProvider, + sendMessageWeb, WA_WEB_AUTH_DIR, + waitForWaConnection, + webAuthExists, } from "../../provider-web.js"; diff --git a/src/twilio/messages.ts b/src/twilio/messages.ts index 2ffa742e2..5c2bd96e9 100644 --- a/src/twilio/messages.ts +++ b/src/twilio/messages.ts @@ -1,17 +1,17 @@ -import { withWhatsAppPrefix } from "../utils.js"; import { readEnv } from "../env.js"; +import { withWhatsAppPrefix } from "../utils.js"; import { createClient } from "./client.js"; export type ListedMessage = { - sid: string; - status: string | null; - direction: string | null; - dateCreated: Date | undefined; - from?: string | null; - to?: string | null; - body?: string | null; - errorCode: number | null; - errorMessage: string | null; + sid: string; + status: string | null; + direction: string | null; + dateCreated: Date | undefined; + from?: string | null; + to?: string | null; + body?: string | null; + errorCode: number | null; + errorMessage: string | null; }; // Remove duplicates by SID while preserving order. @@ -37,54 +37,63 @@ export function sortByDateDesc(messages: ListedMessage[]): ListedMessage[] { // Merge inbound/outbound messages (recent first) for status commands and tests. export async function listRecentMessages( - lookbackMinutes: number, - limit: number, - clientOverride?: ReturnType, + lookbackMinutes: number, + limit: number, + clientOverride?: ReturnType, ): Promise { - const env = readEnv(); - const client = clientOverride ?? createClient(env); - const from = withWhatsAppPrefix(env.whatsappFrom); - const since = new Date(Date.now() - lookbackMinutes * 60_000); + const env = readEnv(); + const client = clientOverride ?? createClient(env); + const from = withWhatsAppPrefix(env.whatsappFrom); + const since = new Date(Date.now() - lookbackMinutes * 60_000); - // Fetch inbound (to our WA number) and outbound (from our WA number), merge, sort, limit. - const fetchLimit = Math.min(Math.max(limit * 2, limit + 10), 100); - const inbound = await client.messages.list({ to: from, dateSentAfter: since, limit: fetchLimit }); - const outbound = await client.messages.list({ from, dateSentAfter: since, limit: fetchLimit }); + // Fetch inbound (to our WA number) and outbound (from our WA number), merge, sort, limit. + const fetchLimit = Math.min(Math.max(limit * 2, limit + 10), 100); + const inbound = await client.messages.list({ + to: from, + dateSentAfter: since, + limit: fetchLimit, + }); + const outbound = await client.messages.list({ + from, + dateSentAfter: since, + limit: fetchLimit, + }); - const inboundArr = Array.isArray(inbound) ? inbound : []; - const outboundArr = Array.isArray(outbound) ? outbound : []; - const combined = uniqueBySid( - [...inboundArr, ...outboundArr].map((m) => ({ - sid: m.sid, - status: m.status ?? null, - direction: m.direction ?? null, - dateCreated: m.dateCreated, - from: m.from, - to: m.to, - body: m.body, - errorCode: m.errorCode ?? null, - errorMessage: m.errorMessage ?? null, - })), - ); + const inboundArr = Array.isArray(inbound) ? inbound : []; + const outboundArr = Array.isArray(outbound) ? outbound : []; + const combined = uniqueBySid( + [...inboundArr, ...outboundArr].map((m) => ({ + sid: m.sid, + status: m.status ?? null, + direction: m.direction ?? null, + dateCreated: m.dateCreated, + from: m.from, + to: m.to, + body: m.body, + errorCode: m.errorCode ?? null, + errorMessage: m.errorMessage ?? null, + })), + ); - return sortByDateDesc(combined).slice(0, limit); + return sortByDateDesc(combined).slice(0, limit); } // Human-friendly single-line formatter for recent messages. export function formatMessageLine(m: ListedMessage): string { - const ts = m.dateCreated?.toISOString() ?? "unknown-time"; - const dir = - m.direction === "inbound" - ? "⬅️ " - : m.direction === "outbound-api" || m.direction === "outbound-reply" - ? "➡️ " - : "↔️ "; - const status = m.status ?? "unknown"; - const err = - m.errorCode != null - ? ` error ${m.errorCode}${m.errorMessage ? ` (${m.errorMessage})` : ""}` - : ""; - const body = (m.body ?? "").replace(/\s+/g, " ").trim(); - const bodyPreview = body.length > 140 ? `${body.slice(0, 137)}…` : body || ""; - return `[${ts}] ${dir}${m.from ?? "?"} -> ${m.to ?? "?"} | ${status}${err} | ${bodyPreview} (sid ${m.sid})`; + const ts = m.dateCreated?.toISOString() ?? "unknown-time"; + const dir = + m.direction === "inbound" + ? "⬅️ " + : m.direction === "outbound-api" || m.direction === "outbound-reply" + ? "➡️ " + : "↔️ "; + const status = m.status ?? "unknown"; + const err = + m.errorCode != null + ? ` error ${m.errorCode}${m.errorMessage ? ` (${m.errorMessage})` : ""}` + : ""; + const body = (m.body ?? "").replace(/\s+/g, " ").trim(); + const bodyPreview = + body.length > 140 ? `${body.slice(0, 137)}…` : body || ""; + return `[${ts}] ${dir}${m.from ?? "?"} -> ${m.to ?? "?"} | ${status}${err} | ${bodyPreview} (sid ${m.sid})`; } diff --git a/src/twilio/monitor.ts b/src/twilio/monitor.ts index 6129ead1d..b7c25ebf6 100644 --- a/src/twilio/monitor.ts +++ b/src/twilio/monitor.ts @@ -1,12 +1,11 @@ import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message.js"; - -import { danger, warn } from "../globals.js"; -import { sleep, withWhatsAppPrefix } from "../utils.js"; -import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { autoReplyIfConfigured } from "../auto-reply/reply.js"; -import { createClient } from "./client.js"; import { readEnv } from "../env.js"; +import { danger } from "../globals.js"; import { logDebug, logInfo, logWarn } from "../logger.js"; +import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { sleep, withWhatsAppPrefix } from "../utils.js"; +import { createClient } from "./client.js"; type MonitorDeps = { autoReplyIfConfigured: typeof autoReplyIfConfigured; @@ -95,7 +94,9 @@ export async function monitorTwilio( lastSeenSid = newestFirst.length ? newestFirst[0].sid : lastSeenSid; iterations += 1; if (iterations >= maxIterations) break; - await deps.sleep(Math.max(pollSeconds, DEFAULT_POLL_INTERVAL_SECONDS) * 1000); + await deps.sleep( + Math.max(pollSeconds, DEFAULT_POLL_INTERVAL_SECONDS) * 1000, + ); } } diff --git a/src/twilio/send.ts b/src/twilio/send.ts index b174f1fc7..3eaedc8e5 100644 --- a/src/twilio/send.ts +++ b/src/twilio/send.ts @@ -1,8 +1,7 @@ -import { success } from "../globals.js"; +import { readEnv } from "../env.js"; import { logInfo } from "../logger.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; -import { withWhatsAppPrefix, sleep } from "../utils.js"; -import { readEnv } from "../env.js"; +import { sleep, withWhatsAppPrefix } from "../utils.js"; import { createClient } from "./client.js"; import { logTwilioSendError } from "./utils.js"; @@ -29,7 +28,10 @@ export async function sendMessage( mediaUrl: opts?.mediaUrl ? [opts.mediaUrl] : undefined, }); - logInfo(`✅ Request accepted. Message SID: ${message.sid} -> ${toNumber}`, runtime); + logInfo( + `✅ Request accepted. Message SID: ${message.sid} -> ${toNumber}`, + runtime, + ); return { client, sid: message.sid }; } catch (err) { logTwilioSendError(err, toNumber, runtime); diff --git a/src/twilio/typing.ts b/src/twilio/typing.ts index fb9d1c6d1..ea4b50975 100644 --- a/src/twilio/typing.ts +++ b/src/twilio/typing.ts @@ -1,4 +1,4 @@ -import { warn, isVerbose, logVerbose } from "../globals.js"; +import { isVerbose, logVerbose, warn } from "../globals.js"; import type { RuntimeEnv } from "../runtime.js"; type TwilioRequestOptions = { diff --git a/src/twilio/update-webhook.ts b/src/twilio/update-webhook.ts index b362e2088..96031e479 100644 --- a/src/twilio/update-webhook.ts +++ b/src/twilio/update-webhook.ts @@ -1,12 +1,13 @@ -import { isVerbose, success, warn } from "../globals.js"; -import { logError, logInfo } from "../logger.js"; import { readEnv } from "../env.js"; -import { normalizeE164 } from "../utils.js"; +import { isVerbose } from "../globals.js"; +import { logError, logInfo } from "../logger.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; -import { createClient } from "./client.js"; -import type { TwilioSenderListClient, TwilioRequester } from "./types.js"; +import type { createClient } from "./client.js"; +import type { TwilioRequester, TwilioSenderListClient } from "./types.js"; -export async function findIncomingNumberSid(client: TwilioSenderListClient): Promise { +export async function findIncomingNumberSid( + client: TwilioSenderListClient, +): Promise { // Look up incoming phone number SID matching the configured WhatsApp number. try { const env = readEnv(); @@ -21,7 +22,9 @@ export async function findIncomingNumberSid(client: TwilioSenderListClient): Pro } } -export async function findMessagingServiceSid(client: TwilioSenderListClient): Promise { +export async function findMessagingServiceSid( + client: TwilioSenderListClient, +): Promise { // Attempt to locate a messaging service tied to the WA phone number (webhook fallback). type IncomingNumberWithService = { messagingServiceSid?: string }; try { @@ -65,7 +68,6 @@ export async function setMessagingServiceWebhook( } } - // Update sender webhook URL with layered fallbacks (channels, form, helper, phone). export async function updateWebhook( client: ReturnType, diff --git a/src/twilio/webhook.ts b/src/twilio/webhook.ts index f6d4944d4..daa9f1ca8 100644 --- a/src/twilio/webhook.ts +++ b/src/twilio/webhook.ts @@ -1,142 +1,158 @@ -import express, { type Request, type Response } from "express"; +import type { Server } from "node:http"; import bodyParser from "body-parser"; import chalk from "chalk"; -import type { Server } from "http"; - -import { success, logVerbose, danger } from "../globals.js"; -import { readEnv, type EnvConfig } from "../env.js"; -import { createClient } from "./client.js"; -import { normalizePath } from "../utils.js"; +import express, { type Request, type Response } from "express"; import { getReplyFromConfig, type ReplyPayload } from "../auto-reply/reply.js"; -import { sendTypingIndicator } from "./typing.js"; -import { logTwilioSendError } from "./utils.js"; -import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { type EnvConfig, readEnv } from "../env.js"; +import { danger, success } from "../globals.js"; +import { ensureMediaHosted } from "../media/host.js"; import { attachMediaRoutes } from "../media/server.js"; import { saveMediaSource } from "../media/store.js"; -import { ensureMediaHosted } from "../media/host.js"; +import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { normalizePath } from "../utils.js"; +import { createClient } from "./client.js"; +import { sendTypingIndicator } from "./typing.js"; +import { logTwilioSendError } from "./utils.js"; /** Start the inbound webhook HTTP server and wire optional auto-replies. */ export async function startWebhook( - port: number, - path = "/webhook/whatsapp", - autoReply: string | undefined, - verbose: boolean, - runtime: RuntimeEnv = defaultRuntime, + port: number, + path = "/webhook/whatsapp", + autoReply: string | undefined, + verbose: boolean, + runtime: RuntimeEnv = defaultRuntime, ): Promise { - const normalizedPath = normalizePath(path); - const env = readEnv(runtime); - const app = express(); + const normalizedPath = normalizePath(path); + const env = readEnv(runtime); + const app = express(); - attachMediaRoutes(app, undefined, runtime); - // Twilio sends application/x-www-form-urlencoded payloads. - app.use(bodyParser.urlencoded({ extended: false })); - app.use((req, _res, next) => { - runtime.log(chalk.gray(`REQ ${req.method} ${req.url}`)); - next(); - }); + attachMediaRoutes(app, undefined, runtime); + // Twilio sends application/x-www-form-urlencoded payloads. + app.use(bodyParser.urlencoded({ extended: false })); + app.use((req, _res, next) => { + runtime.log(chalk.gray(`REQ ${req.method} ${req.url}`)); + next(); + }); - app.post(normalizedPath, async (req: Request, res: Response) => { - const { From, To, Body, MessageSid } = req.body ?? {}; - runtime.log(` + app.post(normalizedPath, async (req: Request, res: Response) => { + const { From, To, Body, MessageSid } = req.body ?? {}; + runtime.log(` [INBOUND] ${From ?? "unknown"} -> ${To ?? "unknown"} (${MessageSid ?? "no-sid"})`); - if (verbose) runtime.log(chalk.gray(`Body: ${Body ?? ""}`)); + if (verbose) runtime.log(chalk.gray(`Body: ${Body ?? ""}`)); - const numMedia = Number.parseInt((req.body?.NumMedia ?? "0") as string, 10); - let mediaPath: string | undefined; - let mediaUrlInbound: string | undefined; - let mediaType: string | undefined; - if (numMedia > 0 && typeof req.body?.MediaUrl0 === "string") { - mediaUrlInbound = req.body.MediaUrl0 as string; - mediaType = typeof req.body?.MediaContentType0 === "string" - ? (req.body.MediaContentType0 as string) - : undefined; - try { - const creds = buildTwilioBasicAuth(env); - const saved = await saveMediaSource(mediaUrlInbound, { - Authorization: `Basic ${creds}`, - }, "inbound"); - mediaPath = saved.path; - if (!mediaType && saved.contentType) mediaType = saved.contentType; - } catch (err) { - runtime.error(danger(`Failed to download inbound media: ${String(err)}`)); - } - } + const numMedia = Number.parseInt((req.body?.NumMedia ?? "0") as string, 10); + let mediaPath: string | undefined; + let mediaUrlInbound: string | undefined; + let mediaType: string | undefined; + if (numMedia > 0 && typeof req.body?.MediaUrl0 === "string") { + mediaUrlInbound = req.body.MediaUrl0 as string; + mediaType = + typeof req.body?.MediaContentType0 === "string" + ? (req.body.MediaContentType0 as string) + : undefined; + try { + const creds = buildTwilioBasicAuth(env); + const saved = await saveMediaSource( + mediaUrlInbound, + { + Authorization: `Basic ${creds}`, + }, + "inbound", + ); + mediaPath = saved.path; + if (!mediaType && saved.contentType) mediaType = saved.contentType; + } catch (err) { + runtime.error( + danger(`Failed to download inbound media: ${String(err)}`), + ); + } + } - const client = createClient(env); - let replyResult: ReplyPayload | undefined = - autoReply !== undefined ? { text: autoReply } : undefined; - if (!replyResult) { - replyResult = await getReplyFromConfig( - { Body, From, To, MessageSid, MediaPath: mediaPath, MediaUrl: mediaUrlInbound, MediaType: mediaType }, - { - onReplyStart: () => sendTypingIndicator(client, runtime, MessageSid), - }, - ); - } + const client = createClient(env); + let replyResult: ReplyPayload | undefined = + autoReply !== undefined ? { text: autoReply } : undefined; + if (!replyResult) { + replyResult = await getReplyFromConfig( + { + Body, + From, + To, + MessageSid, + MediaPath: mediaPath, + MediaUrl: mediaUrlInbound, + MediaType: mediaType, + }, + { + onReplyStart: () => sendTypingIndicator(client, runtime, MessageSid), + }, + ); + } - if (replyResult && (replyResult.text || replyResult.mediaUrl)) { - try { - let mediaUrl = replyResult.mediaUrl; - if (mediaUrl && !/^https?:\/\//i.test(mediaUrl)) { - const hosted = await ensureMediaHosted(mediaUrl); - mediaUrl = hosted.url; - } - await client.messages.create({ - from: To, - to: From, - body: replyResult.text ?? "", - ...(mediaUrl ? { mediaUrl: [mediaUrl] } : {}), - }); - if (verbose) - runtime.log( - success( - `↩️ Auto-replied to ${From}${mediaUrl ? " (media)" : ""}`, - ), - ); - } catch (err) { - logTwilioSendError(err, From ?? undefined, runtime); - } - } + if (replyResult && (replyResult.text || replyResult.mediaUrl)) { + try { + let mediaUrl = replyResult.mediaUrl; + if (mediaUrl && !/^https?:\/\//i.test(mediaUrl)) { + const hosted = await ensureMediaHosted(mediaUrl); + mediaUrl = hosted.url; + } + await client.messages.create({ + from: To, + to: From, + body: replyResult.text ?? "", + ...(mediaUrl ? { mediaUrl: [mediaUrl] } : {}), + }); + if (verbose) + runtime.log( + success(`↩️ Auto-replied to ${From}${mediaUrl ? " (media)" : ""}`), + ); + } catch (err) { + logTwilioSendError(err, From ?? undefined, runtime); + } + } - // Respond 200 OK to Twilio. - res.type("text/xml").send(""); - }); + // Respond 200 OK to Twilio. + res.type("text/xml").send(""); + }); - app.use((_req, res) => { - if (verbose) runtime.log(chalk.yellow(`404 ${_req.method} ${_req.url}`)); - res.status(404).send("warelay webhook: not found"); - }); + app.use((_req, res) => { + if (verbose) runtime.log(chalk.yellow(`404 ${_req.method} ${_req.url}`)); + res.status(404).send("warelay webhook: not found"); + }); - // Start server and resolve once listening; reject on bind error. - return await new Promise((resolve, reject) => { - const server = app.listen(port); + // Start server and resolve once listening; reject on bind error. + return await new Promise((resolve, reject) => { + const server = app.listen(port); - const onListening = () => { - cleanup(); - runtime.log( - `📥 Webhook listening on http://localhost:${port}${normalizedPath}`, - ); - resolve(server); - }; + const onListening = () => { + cleanup(); + runtime.log( + `📥 Webhook listening on http://localhost:${port}${normalizedPath}`, + ); + resolve(server); + }; - const onError = (err: NodeJS.ErrnoException) => { - cleanup(); - reject(err); - }; + const onError = (err: NodeJS.ErrnoException) => { + cleanup(); + reject(err); + }; - const cleanup = () => { - server.off("listening", onListening); - server.off("error", onError); - }; + const cleanup = () => { + server.off("listening", onListening); + server.off("error", onError); + }; - server.once("listening", onListening); - server.once("error", onError); - }); + server.once("listening", onListening); + server.once("error", onError); + }); } function buildTwilioBasicAuth(env: EnvConfig) { if ("authToken" in env.auth) { - return Buffer.from(`${env.accountSid}:${env.auth.authToken}`).toString("base64"); + return Buffer.from(`${env.accountSid}:${env.auth.authToken}`).toString( + "base64", + ); } - return Buffer.from(`${env.auth.apiKey}:${env.auth.apiSecret}`).toString("base64"); + return Buffer.from(`${env.auth.apiKey}:${env.auth.apiSecret}`).toString( + "base64", + ); } diff --git a/src/webhook/server.ts b/src/webhook/server.ts index 8fbaf324a..11d9adf8c 100644 --- a/src/webhook/server.ts +++ b/src/webhook/server.ts @@ -1,4 +1,3 @@ -import type { RuntimeEnv } from "../runtime.js"; import { startWebhook } from "../twilio/webhook.js"; // Thin wrapper to keep webhook server co-located with other webhook helpers. diff --git a/src/webhook/update.ts b/src/webhook/update.ts index 3a8efe72b..0935ed949 100644 --- a/src/webhook/update.ts +++ b/src/webhook/update.ts @@ -1,6 +1,6 @@ export { - updateWebhook, - setMessagingServiceWebhook, findIncomingNumberSid, findMessagingServiceSid, + setMessagingServiceWebhook, + updateWebhook, } from "../twilio/update-webhook.js";