@agenticmail/core 0.9.4 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -78,7 +78,7 @@ var MailSender = class {
78
78
  const code = err?.responseCode ?? err?.code;
79
79
  const isTransient = typeof code === "number" && code >= 400 && code < 500 || code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ESOCKET";
80
80
  if (!isTransient || attempt === MAX_RETRIES) throw err;
81
- await new Promise((resolve) => setTimeout(resolve, 1e3 * (attempt + 1)));
81
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3 * (attempt + 1)));
82
82
  }
83
83
  }
84
84
  throw lastError;
@@ -1010,20 +1010,20 @@ var StalwartAdmin = class {
1010
1010
  }
1011
1011
  try {
1012
1012
  const net = await import("net");
1013
- return await new Promise((resolve) => {
1013
+ return await new Promise((resolve2) => {
1014
1014
  const smtpPort = parseInt(process.env.SMTP_PORT || "25", 10);
1015
1015
  const smtpHost = process.env.SMTP_HOST || "localhost";
1016
1016
  const socket = net.createConnection({ host: smtpHost, port: smtpPort, timeout: 3e3 }, () => {
1017
1017
  socket.destroy();
1018
- resolve(true);
1018
+ resolve2(true);
1019
1019
  });
1020
1020
  socket.on("error", () => {
1021
1021
  socket.destroy();
1022
- resolve(false);
1022
+ resolve2(false);
1023
1023
  });
1024
1024
  socket.on("timeout", () => {
1025
1025
  socket.destroy();
1026
- resolve(false);
1026
+ resolve2(false);
1027
1027
  });
1028
1028
  });
1029
1029
  } catch {
@@ -1095,8 +1095,8 @@ var StalwartAdmin = class {
1095
1095
  }
1096
1096
  const { readFileSync: readFileSync7, writeFileSync: writeFileSync8 } = await import("fs");
1097
1097
  const { homedir: homedir11 } = await import("os");
1098
- const { join: join12 } = await import("path");
1099
- const configPath = join12(homedir11(), ".agenticmail", "stalwart.toml");
1098
+ const { join: join13 } = await import("path");
1099
+ const configPath = join13(homedir11(), ".agenticmail", "stalwart.toml");
1100
1100
  try {
1101
1101
  let config = readFileSync7(configPath, "utf-8");
1102
1102
  config = config.replace(/^hostname\s*=\s*"[^"]*"/m, `hostname = "${escapeTomlString(domain)}"`);
@@ -1110,14 +1110,14 @@ var StalwartAdmin = class {
1110
1110
  /** Path to the host-side stalwart.toml (mounted read-only into container) */
1111
1111
  get configPath() {
1112
1112
  const { homedir: homedir11 } = __require("os");
1113
- const { join: join12 } = __require("path");
1114
- return join12(homedir11(), ".agenticmail", "stalwart.toml");
1113
+ const { join: join13 } = __require("path");
1114
+ return join13(homedir11(), ".agenticmail", "stalwart.toml");
1115
1115
  }
1116
1116
  /** Path to host-side DKIM key directory */
1117
1117
  get dkimDir() {
1118
1118
  const { homedir: homedir11 } = __require("os");
1119
- const { join: join12 } = __require("path");
1120
- return join12(homedir11(), ".agenticmail");
1119
+ const { join: join13 } = __require("path");
1120
+ return join13(homedir11(), ".agenticmail");
1121
1121
  }
1122
1122
  /**
1123
1123
  * Create/reuse a DKIM signing key for a domain.
@@ -1220,9 +1220,9 @@ var StalwartAdmin = class {
1220
1220
  async configureOutboundRelay(config) {
1221
1221
  const { readFileSync: readFileSync7, writeFileSync: writeFileSync8 } = await import("fs");
1222
1222
  const { homedir: homedir11 } = await import("os");
1223
- const { join: join12 } = await import("path");
1223
+ const { join: join13 } = await import("path");
1224
1224
  const routeName = config.routeName ?? "gmail";
1225
- const tomlPath = join12(homedir11(), ".agenticmail", "stalwart.toml");
1225
+ const tomlPath = join13(homedir11(), ".agenticmail", "stalwart.toml");
1226
1226
  let toml = readFileSync7(tomlPath, "utf-8");
1227
1227
  toml = toml.replace(/\n\[queue\.route\.gmail\][\s\S]*?(?=\n\[|$)/, "");
1228
1228
  toml = toml.replace(/\n\[queue\.strategy\][\s\S]*?(?=\n\[|$)/, "");
@@ -2327,7 +2327,8 @@ var OUTBOUND_TEXT_RULES = [
2327
2327
  var HIGH_RISK_EXTENSIONS = /* @__PURE__ */ new Set([".pem", ".key", ".p12", ".pfx", ".env", ".credentials", ".keystore", ".jks", ".p8"]);
2328
2328
  var MEDIUM_RISK_EXTENSIONS = /* @__PURE__ */ new Set([".db", ".sqlite", ".sqlite3", ".sql", ".csv", ".tsv", ".json", ".yml", ".yaml", ".conf", ".config", ".ini"]);
2329
2329
  function stripHtmlTags(html) {
2330
- return html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#0?39;/g, "'").replace(/&nbsp;/g, " ").replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(parseInt(dec, 10)));
2330
+ const bounded = html.length > 1048576 ? html.slice(0, 1048576) : html;
2331
+ return bounded.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#0?39;/g, "'").replace(/&nbsp;/g, " ").replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(parseInt(dec, 10)));
2331
2332
  }
2332
2333
  var TEXT_SCANNABLE_TYPES = /* @__PURE__ */ new Set([
2333
2334
  "text/plain",
@@ -3859,7 +3860,7 @@ var CloudflareClient = class {
3859
3860
  // --- Workers methods ---
3860
3861
  /** Deploy an Email Worker script (ES module format) */
3861
3862
  async deployEmailWorker(scriptName, scriptContent, envVars = {}) {
3862
- const url = `${CF_API_BASE}/accounts/${this.accountId}/workers/scripts/${scriptName}`;
3863
+ const url = `${CF_API_BASE}/accounts/${encodeURIComponent(this.accountId)}/workers/scripts/${encodeURIComponent(scriptName)}`;
3863
3864
  const bindings = Object.entries(envVars).map(([name, text2]) => ({
3864
3865
  type: "plain_text",
3865
3866
  name,
@@ -4277,7 +4278,7 @@ var TunnelManager = class {
4277
4278
  detached: false,
4278
4279
  env: { ...process.env, TUNNEL_TOKEN: tunnelToken }
4279
4280
  });
4280
- await new Promise((resolve, reject) => {
4281
+ await new Promise((resolve2, reject) => {
4281
4282
  let resolved = false;
4282
4283
  const timeout = setTimeout(() => {
4283
4284
  if (!resolved) {
@@ -4291,7 +4292,7 @@ var TunnelManager = class {
4291
4292
  resolved = true;
4292
4293
  clearTimeout(timeout);
4293
4294
  this.running = true;
4294
- resolve();
4295
+ resolve2();
4295
4296
  }
4296
4297
  };
4297
4298
  this.process.stderr?.on("data", onData);
@@ -4330,8 +4331,8 @@ var TunnelManager = class {
4330
4331
  this.process = null;
4331
4332
  p.kill("SIGTERM");
4332
4333
  await Promise.race([
4333
- new Promise((resolve) => p.on("exit", () => resolve())),
4334
- new Promise((resolve) => setTimeout(resolve, 5e3))
4334
+ new Promise((resolve2) => p.on("exit", () => resolve2())),
4335
+ new Promise((resolve2) => setTimeout(resolve2, 5e3))
4335
4336
  ]);
4336
4337
  this.running = false;
4337
4338
  }
@@ -4386,7 +4387,10 @@ function parseGoogleVoiceSms(emailBody, emailFrom) {
4386
4387
  if (!emailBody || typeof emailBody !== "string") return null;
4387
4388
  if (!emailFrom || typeof emailFrom !== "string") return null;
4388
4389
  const fromLower = emailFrom.toLowerCase();
4389
- const isGoogleVoice = fromLower.includes("voice-noreply@google.com") || fromLower.includes("@txt.voice.google.com") || fromLower.includes("voice.google.com") || fromLower.includes("google.com/voice") || fromLower.includes("google") && fromLower.includes("voice");
4390
+ const atIdx = fromLower.lastIndexOf("@");
4391
+ const domain = atIdx >= 0 ? fromLower.slice(atIdx + 1).replace(/[>"'\s].*$/, "") : "";
4392
+ const isGoogleDomain = domain === "google.com" || domain.endsWith(".google.com");
4393
+ const isGoogleVoice = isGoogleDomain && (fromLower.startsWith("voice-noreply@") || domain === "txt.voice.google.com" || domain === "voice.google.com" || domain.endsWith(".voice.google.com") || fromLower.includes("voice"));
4390
4394
  if (!isGoogleVoice) return null;
4391
4395
  let text = emailBody.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/div>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").trim();
4392
4396
  let from = "";
@@ -5417,6 +5421,15 @@ var GatewayManager = class {
5417
5421
  bcc: mail.bcc ? Array.isArray(mail.bcc) ? mail.bcc.join(", ") : mail.bcc : void 0,
5418
5422
  subject: mail.subject,
5419
5423
  text: mail.text || void 0,
5424
+ // The `html` field is the literal HTML body of the outbound
5425
+ // mail — by design it is whatever the sender chose to compose.
5426
+ // CodeQL `js/xss` flags this because the value flows from user
5427
+ // input, but nodemailer is the SMTP serializer, not an HTML
5428
+ // renderer; XSS would only occur if the recipient's MUA
5429
+ // executed the body, which is outside our trust boundary.
5430
+ // The outbound-guard (packages/core/src/mail/outbound-guard.ts)
5431
+ // already scores HTML bodies for suspicious patterns at the
5432
+ // pre-send step. lgtm[js/xss]
5420
5433
  html: mail.html || void 0,
5421
5434
  replyTo: mail.replyTo || from,
5422
5435
  inReplyTo: mail.inReplyTo || void 0,
@@ -5778,11 +5791,11 @@ var RelayBridge = class {
5778
5791
  this.options = options;
5779
5792
  }
5780
5793
  async start() {
5781
- return new Promise((resolve, reject) => {
5794
+ return new Promise((resolve2, reject) => {
5782
5795
  this.server = createServer((req, res) => this.handleRequest(req, res));
5783
5796
  this.server.listen(this.options.port, "127.0.0.1", () => {
5784
5797
  console.log(`[RelayBridge] Listening on 127.0.0.1:${this.options.port}`);
5785
- resolve();
5798
+ resolve2();
5786
5799
  });
5787
5800
  this.server.on("error", reject);
5788
5801
  });
@@ -5984,16 +5997,161 @@ try {
5984
5997
  } catch {
5985
5998
  }
5986
5999
 
6000
+ // src/util/safe-path.ts
6001
+ import { isAbsolute, join as join6, resolve, sep } from "path";
6002
+ var PathTraversalError = class extends Error {
6003
+ constructor(baseDir, parts) {
6004
+ super(
6005
+ `path traversal attempt: ${JSON.stringify(parts)} resolves outside ${baseDir}`
6006
+ );
6007
+ this.baseDir = baseDir;
6008
+ this.parts = parts;
6009
+ this.name = "PathTraversalError";
6010
+ }
6011
+ };
6012
+ function safeJoin(baseDir, ...partsAndOpts) {
6013
+ let opts = {};
6014
+ const parts = [];
6015
+ for (const p of partsAndOpts) {
6016
+ if (typeof p === "string") {
6017
+ parts.push(p);
6018
+ } else if (p && typeof p === "object") {
6019
+ opts = p;
6020
+ }
6021
+ }
6022
+ if (!opts.allowAbsolute) {
6023
+ for (const part of parts) {
6024
+ if (isAbsolute(part)) {
6025
+ throw new PathTraversalError(baseDir, parts);
6026
+ }
6027
+ }
6028
+ }
6029
+ const resolvedBase = resolve(baseDir);
6030
+ const resolved = resolve(resolvedBase, ...parts);
6031
+ if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + sep)) {
6032
+ throw new PathTraversalError(baseDir, parts);
6033
+ }
6034
+ return resolved;
6035
+ }
6036
+ function tryJoin(baseDir, ...parts) {
6037
+ try {
6038
+ return safeJoin(baseDir, ...parts);
6039
+ } catch (err) {
6040
+ if (err instanceof PathTraversalError) return null;
6041
+ throw err;
6042
+ }
6043
+ }
6044
+ function assertWithinBase(baseDir, candidate) {
6045
+ const resolvedBase = resolve(baseDir);
6046
+ const resolvedCandidate = resolve(candidate);
6047
+ if (resolvedCandidate !== resolvedBase && !resolvedCandidate.startsWith(resolvedBase + sep)) {
6048
+ throw new PathTraversalError(baseDir, [candidate]);
6049
+ }
6050
+ return resolvedCandidate;
6051
+ }
6052
+
6053
+ // src/util/redact.ts
6054
+ var REDACTED = "***";
6055
+ function redactSecret(value) {
6056
+ if (typeof value !== "string") return REDACTED;
6057
+ if (value.length === 0) return REDACTED;
6058
+ for (const prefix of ["mk_", "ak_", "sk-", "pk_", "tok_"]) {
6059
+ if (value.startsWith(prefix)) return `${prefix}${REDACTED}`;
6060
+ }
6061
+ return REDACTED;
6062
+ }
6063
+ var SENSITIVE_KEY_PATTERNS = [
6064
+ /key$/i,
6065
+ /secret/i,
6066
+ /password/i,
6067
+ /passwd/i,
6068
+ /token/i,
6069
+ /authorization/i,
6070
+ /bearer/i,
6071
+ /\bapikey\b/i,
6072
+ /\bapi_key\b/i,
6073
+ /\bmasterkey\b/i,
6074
+ /\bmaster_key\b/i
6075
+ ];
6076
+ function looksSensitive(key) {
6077
+ return SENSITIVE_KEY_PATTERNS.some((p) => p.test(key));
6078
+ }
6079
+ function redactObject(input, _depth = 0) {
6080
+ if (_depth > 12) return input;
6081
+ if (input === null || input === void 0) return input;
6082
+ if (typeof input !== "object") return input;
6083
+ if (Array.isArray(input)) {
6084
+ return input.map((v) => redactObject(v, _depth + 1));
6085
+ }
6086
+ const proto = Object.getPrototypeOf(input);
6087
+ if (proto !== Object.prototype && proto !== null) return input;
6088
+ const out = {};
6089
+ for (const [k, v] of Object.entries(input)) {
6090
+ if (looksSensitive(k)) {
6091
+ out[k] = typeof v === "string" ? redactSecret(v) : REDACTED;
6092
+ } else {
6093
+ out[k] = redactObject(v, _depth + 1);
6094
+ }
6095
+ }
6096
+ return out;
6097
+ }
6098
+
6099
+ // src/util/safe-url.ts
6100
+ var UnsafeApiUrlError = class extends Error {
6101
+ constructor(raw, reason) {
6102
+ super(`unsafe AgenticMail API URL ${JSON.stringify(raw)}: ${reason}`);
6103
+ this.raw = raw;
6104
+ this.reason = reason;
6105
+ this.name = "UnsafeApiUrlError";
6106
+ }
6107
+ };
6108
+ var BLOCKED_HOSTS = /* @__PURE__ */ new Set([
6109
+ "169.254.169.254",
6110
+ // AWS / Azure / GCP IPv4 metadata
6111
+ "fd00:ec2::254",
6112
+ // AWS IPv6 metadata
6113
+ "metadata.google.internal",
6114
+ // GCP DNS
6115
+ "metadata.azure.internal"
6116
+ // Azure DNS
6117
+ ]);
6118
+ function validateApiUrl(raw) {
6119
+ if (typeof raw !== "string" || raw.length === 0) {
6120
+ throw new UnsafeApiUrlError(String(raw), "must be a non-empty string");
6121
+ }
6122
+ let parsed;
6123
+ try {
6124
+ parsed = new URL(raw);
6125
+ } catch (err) {
6126
+ throw new UnsafeApiUrlError(raw, `unparseable: ${err.message}`);
6127
+ }
6128
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
6129
+ throw new UnsafeApiUrlError(raw, `unsupported protocol: ${parsed.protocol}`);
6130
+ }
6131
+ const host = parsed.hostname.toLowerCase().replace(/\.$/, "");
6132
+ if (BLOCKED_HOSTS.has(host)) {
6133
+ throw new UnsafeApiUrlError(raw, `blocked metadata host: ${host}`);
6134
+ }
6135
+ if (parsed.username || parsed.password) {
6136
+ throw new UnsafeApiUrlError(raw, "embedded credentials are not supported");
6137
+ }
6138
+ return parsed.origin;
6139
+ }
6140
+ function buildApiUrl(baseOrigin, pathAndQuery) {
6141
+ const path = pathAndQuery.startsWith("/") ? pathAndQuery : `/${pathAndQuery}`;
6142
+ return new URL(path, baseOrigin + "/").toString();
6143
+ }
6144
+
5987
6145
  // src/setup/index.ts
5988
6146
  import { randomBytes as randomBytes3 } from "crypto";
5989
6147
  import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, chmodSync as chmodSync2 } from "fs";
5990
- import { join as join9 } from "path";
6148
+ import { join as join10 } from "path";
5991
6149
  import { homedir as homedir8 } from "os";
5992
6150
 
5993
6151
  // src/setup/deps.ts
5994
6152
  import { execFileSync } from "child_process";
5995
6153
  import { existsSync as existsSync4 } from "fs";
5996
- import { join as join6 } from "path";
6154
+ import { join as join7 } from "path";
5997
6155
  import { homedir as homedir5 } from "os";
5998
6156
  var DependencyChecker = class {
5999
6157
  async checkAll() {
@@ -6044,7 +6202,7 @@ var DependencyChecker = class {
6044
6202
  }
6045
6203
  }
6046
6204
  async checkCloudflared() {
6047
- const binPath = join6(homedir5(), ".agenticmail", "bin", "cloudflared");
6205
+ const binPath = join7(homedir5(), ".agenticmail", "bin", "cloudflared");
6048
6206
  if (existsSync4(binPath)) {
6049
6207
  let version;
6050
6208
  try {
@@ -6069,12 +6227,12 @@ var DependencyChecker = class {
6069
6227
  import { execFileSync as execFileSync2, execSync, spawn as spawnChild } from "child_process";
6070
6228
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, symlinkSync } from "fs";
6071
6229
  import { writeFile, rename, chmod as chmod2, mkdir as mkdir2, unlink } from "fs/promises";
6072
- import { join as join7 } from "path";
6230
+ import { join as join8 } from "path";
6073
6231
  import { homedir as homedir6, platform as platform3, arch as arch2 } from "os";
6074
6232
  function runShellWithRollingOutput(cmd, opts = {}) {
6075
6233
  const maxLines = opts.maxLines ?? 20;
6076
6234
  const timeout = opts.timeout ?? 3e5;
6077
- return new Promise((resolve, reject) => {
6235
+ return new Promise((resolve2, reject) => {
6078
6236
  const child = spawnChild("sh", ["-c", cmd], {
6079
6237
  stdio: ["ignore", "pipe", "pipe"],
6080
6238
  timeout
@@ -6116,7 +6274,7 @@ function runShellWithRollingOutput(cmd, opts = {}) {
6116
6274
  }
6117
6275
  process.stdout.write(`\x1B[${displayedCount}A`);
6118
6276
  }
6119
- resolve({ exitCode: code ?? 1, fullOutput });
6277
+ resolve2({ exitCode: code ?? 1, fullOutput });
6120
6278
  });
6121
6279
  child.on("error", (err) => {
6122
6280
  if (displayedCount > 0) {
@@ -6132,7 +6290,7 @@ function runShellWithRollingOutput(cmd, opts = {}) {
6132
6290
  }
6133
6291
  function runSilent(command, args, opts = {}) {
6134
6292
  const timeout = opts.timeout ?? 3e5;
6135
- return new Promise((resolve, reject) => {
6293
+ return new Promise((resolve2, reject) => {
6136
6294
  const child = spawnChild(command, args, {
6137
6295
  stdio: ["ignore", "pipe", "pipe"],
6138
6296
  timeout
@@ -6144,7 +6302,7 @@ function runSilent(command, args, opts = {}) {
6144
6302
  child.stderr?.on("data", (d) => {
6145
6303
  fullOutput += d.toString();
6146
6304
  });
6147
- child.on("close", (code) => resolve({ exitCode: code ?? 1, fullOutput }));
6305
+ child.on("close", (code) => resolve2({ exitCode: code ?? 1, fullOutput }));
6148
6306
  child.on("error", reject);
6149
6307
  });
6150
6308
  }
@@ -6246,8 +6404,8 @@ var DependencyInstaller = class {
6246
6404
  try {
6247
6405
  const composeBin = execFileSync2("which", ["docker-compose"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
6248
6406
  if (!composeBin) return;
6249
- const pluginDir = join7(homedir6(), ".docker", "cli-plugins");
6250
- const pluginPath = join7(pluginDir, "docker-compose");
6407
+ const pluginDir = join8(homedir6(), ".docker", "cli-plugins");
6408
+ const pluginPath = join8(pluginDir, "docker-compose");
6251
6409
  if (existsSync5(pluginPath)) return;
6252
6410
  try {
6253
6411
  mkdirSync4(pluginDir, { recursive: true });
@@ -6313,9 +6471,9 @@ var DependencyInstaller = class {
6313
6471
  return;
6314
6472
  }
6315
6473
  this.onProgress("__progress__:5:Installing Docker Engine...");
6316
- const tmpDir = join7(homedir6(), ".agenticmail", "tmp");
6474
+ const tmpDir = join8(homedir6(), ".agenticmail", "tmp");
6317
6475
  await mkdir2(tmpDir, { recursive: true });
6318
- const scriptPath = join7(tmpDir, "install-docker.sh");
6476
+ const scriptPath = join8(tmpDir, "install-docker.sh");
6319
6477
  const dlResult = await runShellWithRollingOutput(
6320
6478
  `curl -fsSL https://get.docker.com -o "${scriptPath}" && sudo sh "${scriptPath}"`,
6321
6479
  { timeout: 3e5 }
@@ -6383,7 +6541,7 @@ var DependencyInstaller = class {
6383
6541
  }
6384
6542
  try {
6385
6543
  const programFiles = process.env["ProgramFiles"] || "C:\\Program Files";
6386
- const dockerExe = join7(programFiles, "Docker", "Docker", "Docker Desktop.exe");
6544
+ const dockerExe = join8(programFiles, "Docker", "Docker", "Docker Desktop.exe");
6387
6545
  if (existsSync5(dockerExe)) {
6388
6546
  execSync(`start "" "${dockerExe}"`, { timeout: 1e4, stdio: "ignore", shell: "cmd.exe" });
6389
6547
  }
@@ -6434,7 +6592,7 @@ var DependencyInstaller = class {
6434
6592
  this.onProgress("__progress__:70:Docker Desktop installed. Starting...");
6435
6593
  try {
6436
6594
  const programFiles = process.env["ProgramFiles"] || "C:\\Program Files";
6437
- const dockerExe = join7(programFiles, "Docker", "Docker", "Docker Desktop.exe");
6595
+ const dockerExe = join8(programFiles, "Docker", "Docker", "Docker Desktop.exe");
6438
6596
  if (existsSync5(dockerExe)) {
6439
6597
  execSync(`start "" "${dockerExe}"`, { timeout: 1e4, stdio: "ignore", shell: "cmd.exe" });
6440
6598
  }
@@ -6531,9 +6689,9 @@ var DependencyInstaller = class {
6531
6689
  */
6532
6690
  async installCloudflared() {
6533
6691
  const os = platform3();
6534
- const binDir = join7(homedir6(), ".agenticmail", "bin");
6692
+ const binDir = join8(homedir6(), ".agenticmail", "bin");
6535
6693
  const binName = os === "win32" ? "cloudflared.exe" : "cloudflared";
6536
- const binPath = join7(binDir, binName);
6694
+ const binPath = join8(binDir, binName);
6537
6695
  if (existsSync5(binPath)) {
6538
6696
  return binPath;
6539
6697
  }
@@ -6563,7 +6721,7 @@ var DependencyInstaller = class {
6563
6721
  }
6564
6722
  const buffer = Buffer.from(await response.arrayBuffer());
6565
6723
  if (os === "darwin") {
6566
- const tgzPath = join7(binDir, "cloudflared.tgz");
6724
+ const tgzPath = join8(binDir, "cloudflared.tgz");
6567
6725
  await writeFile(tgzPath, buffer);
6568
6726
  try {
6569
6727
  execFileSync2("tar", ["-xzf", tgzPath, "-C", binDir, "cloudflared"], { timeout: 15e3, stdio: "ignore" });
@@ -6599,7 +6757,7 @@ var DependencyInstaller = class {
6599
6757
  // src/setup/service.ts
6600
6758
  import { execFileSync as execFileSync3, execSync as execSync2 } from "child_process";
6601
6759
  import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync5, chmodSync } from "fs";
6602
- import { join as join8 } from "path";
6760
+ import { join as join9 } from "path";
6603
6761
  import { homedir as homedir7, platform as platform4 } from "os";
6604
6762
  import { createRequire as createRequire2 } from "module";
6605
6763
  var PLIST_LABEL = "com.agenticmail.server";
@@ -6611,9 +6769,9 @@ var ServiceManager = class {
6611
6769
  */
6612
6770
  getServicePath() {
6613
6771
  if (this.os === "darwin") {
6614
- return join8(homedir7(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
6772
+ return join9(homedir7(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
6615
6773
  } else {
6616
- return join8(homedir7(), ".config", "systemd", "user", SYSTEMD_UNIT);
6774
+ return join9(homedir7(), ".config", "systemd", "user", SYSTEMD_UNIT);
6617
6775
  }
6618
6776
  }
6619
6777
  /**
@@ -6651,33 +6809,33 @@ var ServiceManager = class {
6651
6809
  } catch {
6652
6810
  }
6653
6811
  const parentPackages = [
6654
- join8("@agenticmail", "cli"),
6812
+ join9("@agenticmail", "cli"),
6655
6813
  // current scoped package
6656
6814
  "agenticmail"
6657
6815
  // legacy unscoped package
6658
6816
  ];
6659
6817
  const baseDirs = [
6660
6818
  // user-local install
6661
- join8(homedir7(), "node_modules")
6819
+ join9(homedir7(), "node_modules")
6662
6820
  ];
6663
6821
  try {
6664
6822
  const prefix = execSync2("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
6665
- baseDirs.push(join8(prefix, "lib", "node_modules"));
6666
- baseDirs.push(join8(prefix, "node_modules"));
6823
+ baseDirs.push(join9(prefix, "lib", "node_modules"));
6824
+ baseDirs.push(join9(prefix, "node_modules"));
6667
6825
  } catch {
6668
6826
  }
6669
6827
  baseDirs.push("/opt/homebrew/lib/node_modules");
6670
6828
  baseDirs.push("/usr/local/lib/node_modules");
6671
6829
  for (const base of baseDirs) {
6672
- const sibling = join8(base, "@agenticmail", "api", "dist", "index.js");
6830
+ const sibling = join9(base, "@agenticmail", "api", "dist", "index.js");
6673
6831
  if (existsSync6(sibling)) return sibling;
6674
6832
  for (const parent of parentPackages) {
6675
- const nested = join8(base, parent, "node_modules", "@agenticmail", "api", "dist", "index.js");
6833
+ const nested = join9(base, parent, "node_modules", "@agenticmail", "api", "dist", "index.js");
6676
6834
  if (existsSync6(nested)) return nested;
6677
6835
  }
6678
6836
  }
6679
- const dataDir = join8(homedir7(), ".agenticmail");
6680
- const entryCache = join8(dataDir, "api-entry.path");
6837
+ const dataDir = join9(homedir7(), ".agenticmail");
6838
+ const entryCache = join9(dataDir, "api-entry.path");
6681
6839
  if (existsSync6(entryCache)) {
6682
6840
  const cached = readFileSync3(entryCache, "utf-8").trim();
6683
6841
  if (cached && existsSync6(cached)) return cached;
@@ -6688,9 +6846,9 @@ var ServiceManager = class {
6688
6846
  * Cache the API entry path so the service can find it later.
6689
6847
  */
6690
6848
  cacheApiEntryPath(entryPath) {
6691
- const dataDir = join8(homedir7(), ".agenticmail");
6849
+ const dataDir = join9(homedir7(), ".agenticmail");
6692
6850
  if (!existsSync6(dataDir)) mkdirSync5(dataDir, { recursive: true });
6693
- writeFileSync4(join8(dataDir, "api-entry.path"), entryPath);
6851
+ writeFileSync4(join9(dataDir, "api-entry.path"), entryPath);
6694
6852
  }
6695
6853
  /**
6696
6854
  * Get the current package version.
@@ -6711,7 +6869,7 @@ var ServiceManager = class {
6711
6869
  }
6712
6870
  try {
6713
6871
  const apiEntry = this.getApiEntryPath();
6714
- const apiPkg = join8(apiEntry, "..", "..", "package.json");
6872
+ const apiPkg = join9(apiEntry, "..", "..", "package.json");
6715
6873
  if (existsSync6(apiPkg)) {
6716
6874
  const pkg = JSON.parse(readFileSync3(apiPkg, "utf-8"));
6717
6875
  if (pkg.version) return pkg.version;
@@ -6719,14 +6877,14 @@ var ServiceManager = class {
6719
6877
  } catch {
6720
6878
  }
6721
6879
  const candidates = [
6722
- join8(homedir7(), "node_modules", "@agenticmail", "cli", "package.json"),
6723
- join8(homedir7(), "node_modules", "agenticmail", "package.json"),
6724
- join8(homedir7(), ".agenticmail", "package-version.json")
6880
+ join9(homedir7(), "node_modules", "@agenticmail", "cli", "package.json"),
6881
+ join9(homedir7(), "node_modules", "agenticmail", "package.json"),
6882
+ join9(homedir7(), ".agenticmail", "package-version.json")
6725
6883
  ];
6726
6884
  try {
6727
6885
  const prefix = execSync2("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
6728
- candidates.push(join8(prefix, "lib", "node_modules", "@agenticmail", "cli", "package.json"));
6729
- candidates.push(join8(prefix, "lib", "node_modules", "agenticmail", "package.json"));
6886
+ candidates.push(join9(prefix, "lib", "node_modules", "@agenticmail", "cli", "package.json"));
6887
+ candidates.push(join9(prefix, "lib", "node_modules", "agenticmail", "package.json"));
6730
6888
  } catch {
6731
6889
  }
6732
6890
  for (const p of candidates) {
@@ -6745,8 +6903,8 @@ var ServiceManager = class {
6745
6903
  * This ensures AgenticMail doesn't fail on boot when Docker is still loading.
6746
6904
  */
6747
6905
  generateStartScript(nodePath, apiEntry) {
6748
- const scriptPath = join8(homedir7(), ".agenticmail", "bin", "start-server.sh");
6749
- const scriptDir = join8(homedir7(), ".agenticmail", "bin");
6906
+ const scriptPath = join9(homedir7(), ".agenticmail", "bin", "start-server.sh");
6907
+ const scriptDir = join9(homedir7(), ".agenticmail", "bin");
6750
6908
  if (!existsSync6(scriptDir)) mkdirSync5(scriptDir, { recursive: true });
6751
6909
  const script = [
6752
6910
  "#!/bin/bash",
@@ -6812,7 +6970,7 @@ var ServiceManager = class {
6812
6970
  */
6813
6971
  generatePlist(nodePath, apiEntry, configPath) {
6814
6972
  const config = JSON.parse(readFileSync3(configPath, "utf-8"));
6815
- const logDir = join8(homedir7(), ".agenticmail", "logs");
6973
+ const logDir = join9(homedir7(), ".agenticmail", "logs");
6816
6974
  if (!existsSync6(logDir)) mkdirSync5(logDir, { recursive: true });
6817
6975
  const version = this.getVersion();
6818
6976
  const startScript = this.generateStartScript(nodePath, apiEntry);
@@ -6836,7 +6994,7 @@ var ServiceManager = class {
6836
6994
  <key>HOME</key>
6837
6995
  <string>${homedir7()}</string>
6838
6996
  <key>AGENTICMAIL_DATA_DIR</key>
6839
- <string>${config.dataDir || join8(homedir7(), ".agenticmail")}</string>
6997
+ <string>${config.dataDir || join9(homedir7(), ".agenticmail")}</string>
6840
6998
  <key>PATH</key>
6841
6999
  <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
6842
7000
  <key>AGENTICMAIL_SERVICE_VERSION</key>
@@ -6890,7 +7048,7 @@ var ServiceManager = class {
6890
7048
  */
6891
7049
  generateSystemdUnit(nodePath, apiEntry, configPath) {
6892
7050
  const config = JSON.parse(readFileSync3(configPath, "utf-8"));
6893
- const dataDir = config.dataDir || join8(homedir7(), ".agenticmail");
7051
+ const dataDir = config.dataDir || join9(homedir7(), ".agenticmail");
6894
7052
  const version = this.getVersion();
6895
7053
  const startScript = this.generateStartScript(nodePath, apiEntry);
6896
7054
  return `[Unit]
@@ -6920,7 +7078,7 @@ WantedBy=default.target
6920
7078
  * Install the auto-start service.
6921
7079
  */
6922
7080
  install() {
6923
- const configPath = join8(homedir7(), ".agenticmail", "config.json");
7081
+ const configPath = join9(homedir7(), ".agenticmail", "config.json");
6924
7082
  if (!existsSync6(configPath)) {
6925
7083
  return { installed: false, message: "Config not found. Run agenticmail setup first." };
6926
7084
  }
@@ -6933,7 +7091,7 @@ WantedBy=default.target
6933
7091
  }
6934
7092
  const servicePath = this.getServicePath();
6935
7093
  if (this.os === "darwin") {
6936
- const dir = join8(homedir7(), "Library", "LaunchAgents");
7094
+ const dir = join9(homedir7(), "Library", "LaunchAgents");
6937
7095
  if (!existsSync6(dir)) mkdirSync5(dir, { recursive: true });
6938
7096
  if (existsSync6(servicePath)) {
6939
7097
  try {
@@ -6951,7 +7109,7 @@ WantedBy=default.target
6951
7109
  }
6952
7110
  return { installed: true, message: `Service installed at ${servicePath}` };
6953
7111
  } else if (this.os === "linux") {
6954
- const dir = join8(homedir7(), ".config", "systemd", "user");
7112
+ const dir = join9(homedir7(), ".config", "systemd", "user");
6955
7113
  if (!existsSync6(dir)) mkdirSync5(dir, { recursive: true });
6956
7114
  const unit = this.generateSystemdUnit(nodePath, apiEntry, configPath);
6957
7115
  writeFileSync4(servicePath, unit);
@@ -7077,7 +7235,7 @@ WantedBy=default.target
7077
7235
  } catch {
7078
7236
  return { reason: "Service file unreadable" };
7079
7237
  }
7080
- const startScript = join8(homedir7(), ".agenticmail", "bin", "start-server.sh");
7238
+ const startScript = join9(homedir7(), ".agenticmail", "bin", "start-server.sh");
7081
7239
  if (serviceContent.includes(startScript)) {
7082
7240
  if (!existsSync6(startScript)) {
7083
7241
  return { reason: "start-server.sh is missing" };
@@ -7110,7 +7268,7 @@ WantedBy=default.target
7110
7268
  return { reason: `Service version drift (current CLI is v${currentVersion})` };
7111
7269
  }
7112
7270
  }
7113
- const entryCache = join8(homedir7(), ".agenticmail", "api-entry.path");
7271
+ const entryCache = join9(homedir7(), ".agenticmail", "api-entry.path");
7114
7272
  if (existsSync6(entryCache)) {
7115
7273
  try {
7116
7274
  const cached = readFileSync3(entryCache, "utf-8").trim();
@@ -7165,12 +7323,12 @@ var SetupManager = class {
7165
7323
  * falls back to monorepo location.
7166
7324
  */
7167
7325
  getComposePath() {
7168
- const standalonePath = join9(homedir8(), ".agenticmail", "docker-compose.yml");
7326
+ const standalonePath = join10(homedir8(), ".agenticmail", "docker-compose.yml");
7169
7327
  if (existsSync7(standalonePath)) return standalonePath;
7170
7328
  const cwd = process.cwd();
7171
- const candidates = [cwd, join9(cwd, "..")];
7329
+ const candidates = [cwd, join10(cwd, "..")];
7172
7330
  for (const dir of candidates) {
7173
- const p = join9(dir, "docker-compose.yml");
7331
+ const p = join10(dir, "docker-compose.yml");
7174
7332
  if (existsSync7(p)) return p;
7175
7333
  }
7176
7334
  return standalonePath;
@@ -7181,9 +7339,9 @@ var SetupManager = class {
7181
7339
  * Always regenerates Docker files to keep passwords in sync.
7182
7340
  */
7183
7341
  initConfig() {
7184
- const dataDir = join9(homedir8(), ".agenticmail");
7185
- const configPath = join9(dataDir, "config.json");
7186
- const envPath = join9(dataDir, ".env");
7342
+ const dataDir = join10(homedir8(), ".agenticmail");
7343
+ const configPath = join10(dataDir, "config.json");
7344
+ const envPath = join10(dataDir, ".env");
7187
7345
  if (existsSync7(configPath)) {
7188
7346
  try {
7189
7347
  const existing = JSON.parse(readFileSync4(configPath, "utf-8"));
@@ -7235,12 +7393,12 @@ IMAP_PORT=143
7235
7393
  * with the correct admin password from config.
7236
7394
  */
7237
7395
  generateDockerFiles(config) {
7238
- const dataDir = config.dataDir || join9(homedir8(), ".agenticmail");
7396
+ const dataDir = config.dataDir || join10(homedir8(), ".agenticmail");
7239
7397
  if (!existsSync7(dataDir)) {
7240
7398
  mkdirSync6(dataDir, { recursive: true });
7241
7399
  }
7242
7400
  const password = config.stalwart?.adminPassword || "changeme";
7243
- const composePath = join9(dataDir, "docker-compose.yml");
7401
+ const composePath = join10(dataDir, "docker-compose.yml");
7244
7402
  writeFileSync5(composePath, `services:
7245
7403
  stalwart:
7246
7404
  # Pinned to v0.15.5 \u2014 Stalwart 0.16+ moved its config to JSON
@@ -7271,7 +7429,7 @@ volumes:
7271
7429
  stalwart-data:
7272
7430
  `);
7273
7431
  chmodSync2(composePath, 384);
7274
- const tomlPath = join9(dataDir, "stalwart.toml");
7432
+ const tomlPath = join10(dataDir, "stalwart.toml");
7275
7433
  writeFileSync5(tomlPath, `# Stalwart Mail Server \u2014 AgenticMail Configuration
7276
7434
 
7277
7435
  [server]
@@ -7328,7 +7486,7 @@ secret = "${password}"
7328
7486
  * Check if config has already been initialized.
7329
7487
  */
7330
7488
  isInitialized() {
7331
- const configPath = join9(homedir8(), ".agenticmail", "config.json");
7489
+ const configPath = join10(homedir8(), ".agenticmail", "config.json");
7332
7490
  return existsSync7(configPath);
7333
7491
  }
7334
7492
  };
@@ -7336,7 +7494,7 @@ secret = "${password}"
7336
7494
  // src/threading/thread-id.ts
7337
7495
  import { createHash as createHash2 } from "crypto";
7338
7496
  function stripReplyPrefixes(subject) {
7339
- let s = subject;
7497
+ let s = subject.length > 1e3 ? subject.slice(0, 1e3) : subject;
7340
7498
  for (; ; ) {
7341
7499
  const next = s.replace(/^\s*(?:re|fwd?|fw)\s*(?:\[\d+\])?\s*:\s*/i, "");
7342
7500
  if (next === s) break;
@@ -7356,8 +7514,9 @@ function normalizeSubject(subject) {
7356
7514
  }
7357
7515
  function normalizeAddress(addr) {
7358
7516
  if (!addr) return "(unknown)";
7359
- const m = addr.match(/<([^>]+)>/);
7360
- const raw = m ? m[1] : addr;
7517
+ const bounded = addr.length > 500 ? addr.slice(0, 500) : addr;
7518
+ const m = bounded.match(/<([^>]+)>/);
7519
+ const raw = m ? m[1] : bounded;
7361
7520
  return raw.trim().toLowerCase();
7362
7521
  }
7363
7522
  function threadIdFor(input) {
@@ -7377,8 +7536,8 @@ import {
7377
7536
  renameSync
7378
7537
  } from "fs";
7379
7538
  import { homedir as homedir9 } from "os";
7380
- import { join as join10 } from "path";
7381
- var CACHE_DIR_DEFAULT = join10(homedir9(), ".agenticmail", "thread-cache");
7539
+ import { join as join11 } from "path";
7540
+ var CACHE_DIR_DEFAULT = join11(homedir9(), ".agenticmail", "thread-cache");
7382
7541
  var DEFAULT_K_MESSAGES = 10;
7383
7542
  var DEFAULT_LRU_CAP = 5e3;
7384
7543
  var PREVIEW_MAX_CHARS = 240;
@@ -7396,7 +7555,7 @@ var ThreadCache = class {
7396
7555
  }
7397
7556
  }
7398
7557
  pathFor(threadId) {
7399
- return join10(this.dir, `${threadId}.json`);
7558
+ return join11(this.dir, `${threadId}.json`);
7400
7559
  }
7401
7560
  read(threadId) {
7402
7561
  const p = this.pathFor(threadId);
@@ -7486,7 +7645,7 @@ var ThreadCache = class {
7486
7645
  }
7487
7646
  if (files.length <= this.lruCap) return;
7488
7647
  const stats = files.map((f) => {
7489
- const p = join10(this.dir, f);
7648
+ const p = join11(this.dir, f);
7490
7649
  try {
7491
7650
  return { p, mtime: statSync(p).mtimeMs };
7492
7651
  } catch {
@@ -7525,8 +7684,8 @@ import {
7525
7684
  renameSync as renameSync2
7526
7685
  } from "fs";
7527
7686
  import { homedir as homedir10 } from "os";
7528
- import { join as join11 } from "path";
7529
- var MEMORY_DIR_DEFAULT = join11(homedir10(), ".agenticmail", "agent-memory");
7687
+ import { join as join12 } from "path";
7688
+ var MEMORY_DIR_DEFAULT = join12(homedir10(), ".agenticmail", "agent-memory");
7530
7689
  var AgentMemoryStore = class {
7531
7690
  dir;
7532
7691
  constructor(opts = {}) {
@@ -7537,10 +7696,10 @@ var AgentMemoryStore = class {
7537
7696
  }
7538
7697
  }
7539
7698
  dirFor(agentId) {
7540
- return join11(this.dir, sanitizeId(agentId));
7699
+ return join12(this.dir, sanitizeId(agentId));
7541
7700
  }
7542
7701
  pathFor(agentId, threadId) {
7543
- return join11(this.dirFor(agentId), `${sanitizeId(threadId)}.md`);
7702
+ return join12(this.dirFor(agentId), `${sanitizeId(threadId)}.md`);
7544
7703
  }
7545
7704
  read(agentId, threadId) {
7546
7705
  const p = this.pathFor(agentId, threadId);
@@ -7641,6 +7800,8 @@ export {
7641
7800
  InboxWatcher,
7642
7801
  MailReceiver,
7643
7802
  MailSender,
7803
+ PathTraversalError,
7804
+ REDACTED,
7644
7805
  RELAY_PRESETS,
7645
7806
  RelayBridge,
7646
7807
  RelayGateway,
@@ -7652,7 +7813,10 @@ export {
7652
7813
  StalwartAdmin,
7653
7814
  ThreadCache,
7654
7815
  TunnelManager,
7816
+ UnsafeApiUrlError,
7655
7817
  WARNING_THRESHOLD,
7818
+ assertWithinBase,
7819
+ buildApiUrl,
7656
7820
  buildInboundSecurityAdvisory,
7657
7821
  classifyEmailRoute,
7658
7822
  closeDatabase,
@@ -7671,12 +7835,17 @@ export {
7671
7835
  parseEmail,
7672
7836
  parseGoogleVoiceSms,
7673
7837
  recordToolCall,
7838
+ redactObject,
7839
+ redactSecret,
7674
7840
  resolveConfig,
7841
+ safeJoin,
7675
7842
  sanitizeEmail,
7676
7843
  saveConfig,
7677
7844
  scanOutboundEmail,
7678
7845
  scoreEmail,
7679
7846
  setTelemetryVersion,
7680
7847
  startRelayBridge,
7681
- threadIdFor
7848
+ threadIdFor,
7849
+ tryJoin,
7850
+ validateApiUrl
7682
7851
  };