@agenticmail/core 0.9.7 → 0.9.9

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/README.md CHANGED
@@ -188,6 +188,18 @@ Uses your existing Gmail or Outlook account as a middleman. This is the easiest
188
188
 
189
189
  **Deduplication:** The system tracks delivered messages by (message_id, agent_name) to prevent the same email from being delivered twice. Sent message tracking keeps up to 10,000 entries in memory for reply routing.
190
190
 
191
+ ### External inbox exposure — read before enabling the relay
192
+
193
+ > **The RelayGateway makes every sub-agent publicly reachable from the internet** the moment relay mode is configured (typically via `agenticmail setup-email` or the `gateway/relay` POST). This is by design — agents that can email each other locally aren't very useful for talking to real people — but the implications surprise some operators:
194
+
195
+ - **Plus-addresses are publicly guessable.** Anyone who knows your relay address can hit `your-relay+secretary@gmail.com`, `your-relay+kepler@gmail.com`, etc. and the corresponding agent's inbox receives the message. The `+sub` part is not a secret.
196
+ - **External mail wakes the dispatcher identically to internal `@localhost` mail.** When an inbound message lands on a watched account, the dispatcher emits an SSE `new-mail` event and the host integration (`@agenticmail/claudecode`, `@agenticmail/codex`) spawns a worker turn. There's no source-based filter — `bob@gmail.com` and `secretary@localhost` are indistinguishable from the wake path's point of view.
197
+ - **The host bridges take a different path.** Mail to `your-relay+claudecode@gmail.com` / `your-relay+codex@gmail.com` routes to `handleBridgeMail`, which uses the host SDK's `resume` option to wake the operator's last session headlessly, falling through to the bridge-escalation email at `setup_operator_email` if resume fails.
198
+ - **Spam = worker turns.** A scraper that guesses a plus-address can drive Claude / Codex invocations at the operator's expense. Throttles in order of escalation:
199
+ 1. The `wake-budget` guard in `dispatcher.handleEvent` (default cap per minute per agent — kicks in automatically).
200
+ 2. Relay-layer spam filtering — the built-in spam filter runs on inbound mail before publishing the SSE event.
201
+ 3. For agents that should be internal-only, set `metadata.host` to a value no dispatcher matches, so the SSE event fires but no host wakes on it.
202
+
191
203
  ### Domain Mode
192
204
 
193
205
  Full custom domain through Cloudflare. Agents send from `agent@yourdomain.com` with proper email authentication (DKIM, SPF, DMARC).
package/dist/index.cjs CHANGED
@@ -756,11 +756,13 @@ __export(index_exports, {
756
756
  forgetHostSession: () => forgetHostSession,
757
757
  getDatabase: () => getDatabase,
758
758
  getOperatorEmail: () => getOperatorEmail,
759
+ getSmsProvider: () => getSmsProvider,
759
760
  hostSessionStoragePath: () => hostSessionStoragePath,
760
761
  isInternalEmail: () => isInternalEmail,
761
762
  isSessionFresh: () => isSessionFresh,
762
763
  isValidPhoneNumber: () => isValidPhoneNumber,
763
764
  loadHostSession: () => loadHostSession,
765
+ mapProviderSmsStatus: () => mapProviderSmsStatus,
764
766
  normalizeAddress: () => normalizeAddress,
765
767
  normalizePhoneNumber: () => normalizePhoneNumber,
766
768
  normalizeSubject: () => normalizeSubject,
@@ -770,6 +772,7 @@ __export(index_exports, {
770
772
  recordToolCall: () => recordToolCall,
771
773
  redactObject: () => redactObject,
772
774
  redactSecret: () => redactSecret,
775
+ redactSmsConfig: () => redactSmsConfig,
773
776
  resolveConfig: () => resolveConfig,
774
777
  safeJoin: () => safeJoin,
775
778
  sanitizeEmail: () => sanitizeEmail,
@@ -969,6 +972,14 @@ var MailReceiver = class {
969
972
  name: a.name,
970
973
  address: a.address ?? ""
971
974
  })),
975
+ cc: (env.cc ?? []).map((a) => ({
976
+ name: a.name,
977
+ address: a.address ?? ""
978
+ })),
979
+ bcc: (env.bcc ?? []).map((a) => ({
980
+ name: a.name,
981
+ address: a.address ?? ""
982
+ })),
972
983
  date: env.date ?? /* @__PURE__ */ new Date(),
973
984
  flags: msg.flags ?? /* @__PURE__ */ new Set(),
974
985
  size: msg.size ?? 0
@@ -3871,7 +3882,47 @@ var DomainManager = class {
3871
3882
 
3872
3883
  // src/gateway/manager.ts
3873
3884
  var import_node_path4 = require("path");
3885
+
3886
+ // src/crypto/secrets.ts
3874
3887
  var import_node_crypto2 = require("crypto");
3888
+ function deriveKey(key, salt) {
3889
+ return (0, import_node_crypto2.scryptSync)(key, salt, 32, { N: 16384, r: 8, p: 1 });
3890
+ }
3891
+ function encryptSecret(plaintext, key) {
3892
+ const salt = (0, import_node_crypto2.randomBytes)(16);
3893
+ const derivedKey = deriveKey(key, salt);
3894
+ const iv = (0, import_node_crypto2.randomBytes)(12);
3895
+ const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", derivedKey, iv);
3896
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
3897
+ const authTag = cipher.getAuthTag();
3898
+ return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
3899
+ }
3900
+ function decryptSecret(value, key) {
3901
+ if (value.startsWith("enc2:")) {
3902
+ const parts = value.split(":");
3903
+ if (parts.length !== 5) return value;
3904
+ const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
3905
+ const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
3906
+ const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
3907
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
3908
+ return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
3909
+ }
3910
+ if (value.startsWith("enc:")) {
3911
+ const parts = value.split(":");
3912
+ if (parts.length !== 4) return value;
3913
+ const [, ivHex, authTagHex, ciphertextHex] = parts;
3914
+ const keyHash = (0, import_node_crypto2.createHash)("sha256").update(key).digest();
3915
+ const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
3916
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
3917
+ return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
3918
+ }
3919
+ return value;
3920
+ }
3921
+ function isEncryptedSecret(value) {
3922
+ return typeof value === "string" && (value.startsWith("enc2:") || value.startsWith("enc:"));
3923
+ }
3924
+
3925
+ // src/gateway/manager.ts
3875
3926
  var import_nodemailer3 = __toESM(require("nodemailer"), 1);
3876
3927
 
3877
3928
  // src/debug.ts
@@ -5149,6 +5200,130 @@ var import_mail_composer3 = __toESM(require("nodemailer/lib/mail-composer/index.
5149
5200
  init_spam_filter();
5150
5201
 
5151
5202
  // src/sms/manager.ts
5203
+ function asString(value) {
5204
+ return typeof value === "string" ? value.trim() : "";
5205
+ }
5206
+ function defaultApiUrl(config) {
5207
+ const url = (config.apiUrl || "https://api.46elks.com/a1").replace(/\/+$/, "");
5208
+ if (!/^https:\/\//i.test(url)) {
5209
+ throw new Error("46elks apiUrl must use https:// \u2014 refusing to send credentials over a non-TLS connection");
5210
+ }
5211
+ return url;
5212
+ }
5213
+ function basicAuth(username, password) {
5214
+ return Buffer.from(`${username}:${password}`, "utf8").toString("base64");
5215
+ }
5216
+ function redactSmsConfig(config) {
5217
+ return {
5218
+ ...config,
5219
+ forwardingPassword: config.forwardingPassword ? "***" : void 0,
5220
+ password: config.password ? "***" : void 0,
5221
+ webhookSecret: config.webhookSecret ? "***" : void 0
5222
+ };
5223
+ }
5224
+ var GoogleVoiceSmsProvider = class {
5225
+ id = "google_voice";
5226
+ async sendSms(config, input) {
5227
+ const to = normalizePhoneNumber(input.to);
5228
+ const from = normalizePhoneNumber(config.phoneNumber);
5229
+ if (!to) throw new Error("Invalid recipient phone number");
5230
+ if (!from) throw new Error("Invalid configured Google Voice phone number");
5231
+ return {
5232
+ provider: this.id,
5233
+ status: "pending",
5234
+ from,
5235
+ to,
5236
+ body: input.body,
5237
+ raw: {
5238
+ delivery: "manual_google_voice_web",
5239
+ url: "https://voice.google.com"
5240
+ }
5241
+ };
5242
+ }
5243
+ parseInboundSms() {
5244
+ return null;
5245
+ }
5246
+ };
5247
+ var FortySixElksSmsProvider = class {
5248
+ id = "46elks";
5249
+ async sendSms(config, input) {
5250
+ const username = asString(config.username);
5251
+ const password = asString(config.password);
5252
+ if (!username || !password) {
5253
+ throw new Error("46elks username and password are required");
5254
+ }
5255
+ const to = normalizePhoneNumber(input.to);
5256
+ const from = normalizePhoneNumber(config.phoneNumber);
5257
+ if (!to) throw new Error("Invalid recipient phone number");
5258
+ if (!from) throw new Error("Invalid configured 46elks phone number");
5259
+ const form = new URLSearchParams();
5260
+ form.set("to", to);
5261
+ form.set("from", from);
5262
+ form.set("message", input.body);
5263
+ if (input.dryRun) form.set("dryrun", "yes");
5264
+ const response = await fetch(`${defaultApiUrl(config)}/sms`, {
5265
+ method: "POST",
5266
+ headers: {
5267
+ "Authorization": `Basic ${basicAuth(username, password)}`,
5268
+ "Content-Type": "application/x-www-form-urlencoded"
5269
+ },
5270
+ body: form,
5271
+ signal: AbortSignal.timeout(15e3)
5272
+ });
5273
+ const text = await response.text();
5274
+ let raw = text;
5275
+ try {
5276
+ raw = JSON.parse(text);
5277
+ } catch {
5278
+ }
5279
+ if (!response.ok) {
5280
+ const message = typeof raw === "object" && raw && ("message" in raw || "error" in raw) ? String(raw.message ?? raw.error) : text.slice(0, 200);
5281
+ throw new Error(`46elks SMS failed (${response.status}): ${message}`);
5282
+ }
5283
+ const providerId = typeof raw === "object" && raw && "id" in raw ? String(raw.id) : void 0;
5284
+ const providerStatus = typeof raw === "object" && raw && "status" in raw ? String(raw.status) : "sent";
5285
+ return {
5286
+ provider: this.id,
5287
+ id: providerId,
5288
+ status: providerStatus,
5289
+ from,
5290
+ to,
5291
+ body: input.body,
5292
+ raw
5293
+ };
5294
+ }
5295
+ parseInboundSms(payload) {
5296
+ const direction = asString(payload.direction).toLowerCase();
5297
+ if (direction && direction !== "incoming") return null;
5298
+ const from = normalizePhoneNumber(asString(payload.from));
5299
+ const to = normalizePhoneNumber(asString(payload.to));
5300
+ const body = asString(payload.message);
5301
+ if (!from || !to || !body) return null;
5302
+ return {
5303
+ provider: this.id,
5304
+ id: asString(payload.id) || void 0,
5305
+ from,
5306
+ to,
5307
+ body,
5308
+ timestamp: asString(payload.created) || (/* @__PURE__ */ new Date()).toISOString(),
5309
+ raw: payload
5310
+ };
5311
+ }
5312
+ };
5313
+ var PROVIDERS = {
5314
+ google_voice: new GoogleVoiceSmsProvider(),
5315
+ "46elks": new FortySixElksSmsProvider()
5316
+ };
5317
+ function getSmsProvider(provider) {
5318
+ return PROVIDERS[provider];
5319
+ }
5320
+ function mapProviderSmsStatus(status) {
5321
+ const normalized = status.toLowerCase();
5322
+ if (normalized === "delivered") return "delivered";
5323
+ if (normalized === "failed" || normalized === "error") return "failed";
5324
+ if (normalized === "created" || normalized === "queued" || normalized === "sent") return "sent";
5325
+ return "sent";
5326
+ }
5152
5327
  function normalizePhoneNumber(raw) {
5153
5328
  const cleaned = raw.replace(/[^+\d]/g, "");
5154
5329
  if (!cleaned) return null;
@@ -5255,12 +5430,48 @@ function extractVerificationCode(smsBody) {
5255
5430
  }
5256
5431
  return null;
5257
5432
  }
5433
+ var SMS_SECRET_FIELDS = ["password", "webhookSecret", "forwardingPassword"];
5258
5434
  var SmsManager = class {
5259
- constructor(db2) {
5435
+ /**
5436
+ * Optional master key used to encrypt SMS credentials at rest (same
5437
+ * AES-256-GCM scheme GatewayManager uses for relay/domain secrets).
5438
+ * When absent (e.g. tests, or a deployment with no master key) configs
5439
+ * are stored as-is and reads tolerate plaintext — so upgrades and
5440
+ * downgrades both stay safe.
5441
+ */
5442
+ constructor(db2, encryptionKey) {
5260
5443
  this.db = db2;
5444
+ this.encryptionKey = encryptionKey;
5261
5445
  this.ensureTable();
5262
5446
  }
5263
5447
  initialized = false;
5448
+ /** Encrypt the credential fields of an SMS config before persisting. */
5449
+ encryptConfig(config) {
5450
+ if (!this.encryptionKey) return config;
5451
+ const out = { ...config };
5452
+ for (const field of SMS_SECRET_FIELDS) {
5453
+ const value = out[field];
5454
+ if (typeof value === "string" && value && !isEncryptedSecret(value)) {
5455
+ out[field] = encryptSecret(value, this.encryptionKey);
5456
+ }
5457
+ }
5458
+ return out;
5459
+ }
5460
+ /** Decrypt the credential fields of an SMS config after loading. */
5461
+ decryptConfig(config) {
5462
+ if (!this.encryptionKey) return config;
5463
+ const out = { ...config };
5464
+ for (const field of SMS_SECRET_FIELDS) {
5465
+ const value = out[field];
5466
+ if (typeof value === "string" && isEncryptedSecret(value)) {
5467
+ try {
5468
+ out[field] = decryptSecret(value, this.encryptionKey);
5469
+ } catch {
5470
+ }
5471
+ }
5472
+ }
5473
+ return out;
5474
+ }
5264
5475
  ensureTable() {
5265
5476
  if (this.initialized) return;
5266
5477
  try {
@@ -5293,18 +5504,19 @@ var SmsManager = class {
5293
5504
  this.initialized = true;
5294
5505
  }
5295
5506
  }
5296
- /** Get SMS config from agent metadata */
5507
+ /** Get SMS config from agent metadata (credential fields decrypted). */
5297
5508
  getSmsConfig(agentId) {
5298
5509
  const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
5299
5510
  if (!row) return null;
5300
5511
  try {
5301
5512
  const meta = JSON.parse(row.metadata || "{}");
5302
- return meta.sms && meta.sms.enabled !== void 0 ? meta.sms : null;
5513
+ if (!meta.sms || meta.sms.enabled === void 0) return null;
5514
+ return this.decryptConfig(meta.sms);
5303
5515
  } catch {
5304
5516
  return null;
5305
5517
  }
5306
5518
  }
5307
- /** Save SMS config to agent metadata */
5519
+ /** Save SMS config to agent metadata (credential fields encrypted). */
5308
5520
  saveSmsConfig(agentId, config) {
5309
5521
  const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
5310
5522
  if (!row) throw new Error(`Agent ${agentId} not found`);
@@ -5314,7 +5526,7 @@ var SmsManager = class {
5314
5526
  } catch {
5315
5527
  meta = {};
5316
5528
  }
5317
- meta.sms = config;
5529
+ meta.sms = this.encryptConfig(config);
5318
5530
  this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
5319
5531
  }
5320
5532
  /**
@@ -5357,27 +5569,50 @@ var SmsManager = class {
5357
5569
  delete meta.sms;
5358
5570
  this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
5359
5571
  }
5360
- /** Record an inbound SMS (parsed from email) */
5361
- recordInbound(agentId, parsed) {
5572
+ /** Find the agent whose SMS config owns a phone number. */
5573
+ findAgentBySmsNumber(phoneNumber, provider) {
5574
+ const normalized = normalizePhoneNumber(phoneNumber);
5575
+ if (!normalized) return null;
5576
+ const rows = this.db.prepare("SELECT id, metadata FROM agents").all();
5577
+ for (const row of rows) {
5578
+ try {
5579
+ const meta = JSON.parse(row.metadata || "{}");
5580
+ const cfg = meta.sms;
5581
+ if (!cfg?.enabled) continue;
5582
+ if (provider && cfg.provider !== provider) continue;
5583
+ if (normalizePhoneNumber(cfg.phoneNumber) === normalized) {
5584
+ return { agentId: row.id, config: this.decryptConfig(cfg) };
5585
+ }
5586
+ } catch {
5587
+ }
5588
+ }
5589
+ return null;
5590
+ }
5591
+ /** Record an inbound SMS (parsed from email or provider webhook) */
5592
+ recordInbound(agentId, parsed, metadata) {
5362
5593
  const id = `sms_in_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
5363
5594
  const createdAt = parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString();
5364
5595
  this.db.prepare(
5365
- "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
5366
- ).run(id, agentId, "inbound", parsed.from, parsed.body, "received", createdAt);
5367
- return { id, agentId, direction: "inbound", phoneNumber: parsed.from, body: parsed.body, status: "received", createdAt };
5596
+ "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
5597
+ ).run(id, agentId, "inbound", parsed.from, parsed.body, "received", createdAt, JSON.stringify(metadata ?? {}));
5598
+ return { id, agentId, direction: "inbound", phoneNumber: parsed.from, body: parsed.body, status: "received", createdAt, metadata };
5368
5599
  }
5369
5600
  /** Record an outbound SMS attempt */
5370
- recordOutbound(agentId, phoneNumber, body, status = "pending") {
5601
+ recordOutbound(agentId, phoneNumber, body, status = "pending", metadata) {
5371
5602
  const id = `sms_out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
5372
5603
  const now = (/* @__PURE__ */ new Date()).toISOString();
5373
5604
  const normalized = normalizePhoneNumber(phoneNumber) || phoneNumber;
5374
5605
  this.db.prepare(
5375
- "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
5376
- ).run(id, agentId, "outbound", normalized, body, status, now);
5377
- return { id, agentId, direction: "outbound", phoneNumber: normalized, body, status, createdAt: now };
5378
- }
5379
- /** Update SMS status */
5380
- updateStatus(id, status) {
5606
+ "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
5607
+ ).run(id, agentId, "outbound", normalized, body, status, now, JSON.stringify(metadata ?? {}));
5608
+ return { id, agentId, direction: "outbound", phoneNumber: normalized, body, status, createdAt: now, metadata };
5609
+ }
5610
+ /** Update SMS status and optional provider metadata */
5611
+ updateStatus(id, status, metadata) {
5612
+ if (metadata) {
5613
+ this.db.prepare("UPDATE sms_messages SET status = ?, metadata = ? WHERE id = ?").run(status, JSON.stringify(metadata), id);
5614
+ return;
5615
+ }
5381
5616
  this.db.prepare("UPDATE sms_messages SET status = ? WHERE id = ?").run(status, id);
5382
5617
  }
5383
5618
  /** List SMS messages for an agent */
@@ -5577,39 +5812,6 @@ var SmsPoller = class {
5577
5812
  };
5578
5813
 
5579
5814
  // src/gateway/manager.ts
5580
- function deriveKey(key, salt) {
5581
- return (0, import_node_crypto2.scryptSync)(key, salt, 32, { N: 16384, r: 8, p: 1 });
5582
- }
5583
- function encryptSecret(plaintext, key) {
5584
- const salt = (0, import_node_crypto2.randomBytes)(16);
5585
- const derivedKey = deriveKey(key, salt);
5586
- const iv = (0, import_node_crypto2.randomBytes)(12);
5587
- const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", derivedKey, iv);
5588
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
5589
- const authTag = cipher.getAuthTag();
5590
- return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
5591
- }
5592
- function decryptSecret(value, key) {
5593
- if (value.startsWith("enc2:")) {
5594
- const parts = value.split(":");
5595
- if (parts.length !== 5) return value;
5596
- const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
5597
- const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
5598
- const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
5599
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
5600
- return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
5601
- }
5602
- if (value.startsWith("enc:")) {
5603
- const parts = value.split(":");
5604
- if (parts.length !== 4) return value;
5605
- const [, ivHex, authTagHex, ciphertextHex] = parts;
5606
- const keyHash = (0, import_node_crypto2.createHash)("sha256").update(key).digest();
5607
- const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
5608
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
5609
- return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
5610
- }
5611
- return value;
5612
- }
5613
5815
  var GatewayManager = class {
5614
5816
  constructor(options) {
5615
5817
  this.options = options;
@@ -8751,11 +8953,13 @@ function parse(raw) {
8751
8953
  forgetHostSession,
8752
8954
  getDatabase,
8753
8955
  getOperatorEmail,
8956
+ getSmsProvider,
8754
8957
  hostSessionStoragePath,
8755
8958
  isInternalEmail,
8756
8959
  isSessionFresh,
8757
8960
  isValidPhoneNumber,
8758
8961
  loadHostSession,
8962
+ mapProviderSmsStatus,
8759
8963
  normalizeAddress,
8760
8964
  normalizePhoneNumber,
8761
8965
  normalizeSubject,
@@ -8765,6 +8969,7 @@ function parse(raw) {
8765
8969
  recordToolCall,
8766
8970
  redactObject,
8767
8971
  redactSecret,
8972
+ redactSmsConfig,
8768
8973
  resolveConfig,
8769
8974
  safeJoin,
8770
8975
  sanitizeEmail,
package/dist/index.d.cts CHANGED
@@ -37,6 +37,12 @@ interface EmailEnvelope {
37
37
  subject: string;
38
38
  from: AddressInfo[];
39
39
  to: AddressInfo[];
40
+ /** Cc/Bcc from the IMAP ENVELOPE response. Added so message-view's
41
+ * quote-audience backfill can pull the previous rounds' Cc/Bcc
42
+ * without a full FETCH BODY round-trip — ENVELOPE already carries
43
+ * them, we just weren't surfacing them. */
44
+ cc?: AddressInfo[];
45
+ bcc?: AddressInfo[];
40
46
  date: Date;
41
47
  flags: Set<string>;
42
48
  size: number;
@@ -1436,13 +1442,13 @@ declare class RelayBridge {
1436
1442
  declare function startRelayBridge(options: RelayBridgeOptions): RelayBridge;
1437
1443
 
1438
1444
  /**
1439
- * SMS Manager - Google Voice SMS integration
1445
+ * SMS Manager - provider-backed SMS integration
1440
1446
  *
1441
1447
  * How it works:
1442
- * 1. User sets up Google Voice with SMS-to-email forwarding
1443
- * 2. Incoming SMS arrives at Google Voice -> forwarded to email -> lands in agent inbox
1444
- * 3. Agent parses forwarded SMS from email body
1445
- * 4. Outgoing SMS sent via Google Voice web interface (browser automation)
1448
+ * 1. User chooses a provider for the agent phone number
1449
+ * 2. Google Voice uses email forwarding/web instructions
1450
+ * 3. 46elks uses direct API sends and inbound webhooks
1451
+ * 4. All inbound/outbound messages are stored in the SMS table
1446
1452
  *
1447
1453
  * SMS config is stored in agent metadata under the "sms" key.
1448
1454
  */
@@ -1450,16 +1456,24 @@ declare function startRelayBridge(options: RelayBridgeOptions): RelayBridge;
1450
1456
  interface SmsConfig {
1451
1457
  /** Whether SMS is enabled for this agent */
1452
1458
  enabled: boolean;
1453
- /** Google Voice phone number (e.g. +12125551234) */
1459
+ /** Phone number in E.164 format where possible */
1454
1460
  phoneNumber: string;
1455
1461
  /** The email address Google Voice forwards SMS to (the Gmail used for GV signup) */
1456
- forwardingEmail: string;
1462
+ forwardingEmail?: string;
1457
1463
  /** App password for forwarding email (only needed if different from relay email) */
1458
1464
  forwardingPassword?: string;
1459
1465
  /** Whether the GV Gmail is the same as the relay email */
1460
1466
  sameAsRelay?: boolean;
1461
- /** Provider (currently only google_voice) */
1462
- provider: 'google_voice';
1467
+ /** SMS provider */
1468
+ provider: 'google_voice' | '46elks';
1469
+ /** 46elks API username */
1470
+ username?: string;
1471
+ /** 46elks API password */
1472
+ password?: string;
1473
+ /** Provider API base URL override */
1474
+ apiUrl?: string;
1475
+ /** Secret required on inbound provider webhooks */
1476
+ webhookSecret?: string;
1463
1477
  /** When SMS was configured */
1464
1478
  configuredAt: string;
1465
1479
  }
@@ -1479,6 +1493,37 @@ interface SmsMessage {
1479
1493
  createdAt: string;
1480
1494
  metadata?: Record<string, unknown>;
1481
1495
  }
1496
+ interface SendSmsInput {
1497
+ to: string;
1498
+ body: string;
1499
+ dryRun?: boolean;
1500
+ }
1501
+ interface SendSmsResult {
1502
+ provider: SmsConfig['provider'];
1503
+ id?: string;
1504
+ status: string;
1505
+ from: string;
1506
+ to: string;
1507
+ body: string;
1508
+ raw?: unknown;
1509
+ }
1510
+ interface InboundSmsEvent {
1511
+ provider: SmsConfig['provider'];
1512
+ id?: string;
1513
+ from: string;
1514
+ to: string;
1515
+ body: string;
1516
+ timestamp: string;
1517
+ raw?: unknown;
1518
+ }
1519
+ interface SmsProvider {
1520
+ id: SmsConfig['provider'];
1521
+ sendSms(config: SmsConfig, input: SendSmsInput): Promise<SendSmsResult>;
1522
+ parseInboundSms(payload: Record<string, unknown>): InboundSmsEvent | null;
1523
+ }
1524
+ declare function redactSmsConfig(config: SmsConfig): SmsConfig;
1525
+ declare function getSmsProvider(provider: SmsConfig['provider']): SmsProvider;
1526
+ declare function mapProviderSmsStatus(status: string): SmsMessage['status'];
1482
1527
  /** Normalize a phone number to E.164-ish format (+1XXXXXXXXXX) */
1483
1528
  declare function normalizePhoneNumber(raw: string): string | null;
1484
1529
  /** Validate a phone number (basic) */
@@ -1500,12 +1545,24 @@ declare function parseGoogleVoiceSms(emailBody: string, emailFrom: string): Pars
1500
1545
  declare function extractVerificationCode(smsBody: string): string | null;
1501
1546
  declare class SmsManager {
1502
1547
  private db;
1548
+ private encryptionKey?;
1503
1549
  private initialized;
1504
- constructor(db: Database);
1550
+ /**
1551
+ * Optional master key used to encrypt SMS credentials at rest (same
1552
+ * AES-256-GCM scheme GatewayManager uses for relay/domain secrets).
1553
+ * When absent (e.g. tests, or a deployment with no master key) configs
1554
+ * are stored as-is and reads tolerate plaintext — so upgrades and
1555
+ * downgrades both stay safe.
1556
+ */
1557
+ constructor(db: Database, encryptionKey?: string | undefined);
1558
+ /** Encrypt the credential fields of an SMS config before persisting. */
1559
+ private encryptConfig;
1560
+ /** Decrypt the credential fields of an SMS config after loading. */
1561
+ private decryptConfig;
1505
1562
  private ensureTable;
1506
- /** Get SMS config from agent metadata */
1563
+ /** Get SMS config from agent metadata (credential fields decrypted). */
1507
1564
  getSmsConfig(agentId: string): SmsConfig | null;
1508
- /** Save SMS config to agent metadata */
1565
+ /** Save SMS config to agent metadata (credential fields encrypted). */
1509
1566
  saveSmsConfig(agentId: string, config: SmsConfig): void;
1510
1567
  /**
1511
1568
  * Resolve the operator's "where do I get pinged" address from an
@@ -1531,12 +1588,17 @@ declare class SmsManager {
1531
1588
  getAlertEmail(agentId: string): string | null;
1532
1589
  /** Remove SMS config from agent metadata */
1533
1590
  removeSmsConfig(agentId: string): void;
1534
- /** Record an inbound SMS (parsed from email) */
1535
- recordInbound(agentId: string, parsed: ParsedSms): SmsMessage;
1591
+ /** Find the agent whose SMS config owns a phone number. */
1592
+ findAgentBySmsNumber(phoneNumber: string, provider?: SmsConfig['provider']): {
1593
+ agentId: string;
1594
+ config: SmsConfig;
1595
+ } | null;
1596
+ /** Record an inbound SMS (parsed from email or provider webhook) */
1597
+ recordInbound(agentId: string, parsed: ParsedSms, metadata?: Record<string, unknown>): SmsMessage;
1536
1598
  /** Record an outbound SMS attempt */
1537
- recordOutbound(agentId: string, phoneNumber: string, body: string, status?: 'pending' | 'sent' | 'failed'): SmsMessage;
1538
- /** Update SMS status */
1539
- updateStatus(id: string, status: SmsMessage['status']): void;
1599
+ recordOutbound(agentId: string, phoneNumber: string, body: string, status?: 'pending' | 'sent' | 'failed', metadata?: Record<string, unknown>): SmsMessage;
1600
+ /** Update SMS status and optional provider metadata */
1601
+ updateStatus(id: string, status: SmsMessage['status'], metadata?: Record<string, unknown>): void;
1540
1602
  /** List SMS messages for an agent */
1541
1603
  listMessages(agentId: string, opts?: {
1542
1604
  direction?: 'inbound' | 'outbound';
@@ -2562,4 +2624,4 @@ declare class AgentMemoryStore {
2562
2624
  renderForPrompt(memory: AgentMemoryRead | null): string;
2563
2625
  }
2564
2626
 
2565
- export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentMemoryFields, type AgentMemoryOptions, type AgentMemoryRead, AgentMemoryStore, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, type CachedMessage, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DEFAULT_SESSION_MAX_AGE_MS, DNSConfigurator, type Database, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, type EmailRouteAction, type EmailRouteClass, type EmailRouteClassification, type EmailRouteInput, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type HostName, type HostSession, type InboundEmail, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, PathTraversalError, type PurchasedDomain, REDACTED, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, SPAM_THRESHOLD, type SafeJoinOptions, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, ThreadCache, type ThreadCacheEntry, type ThreadCacheOptions, type ThreadIdInput, type TunnelConfig, TunnelManager, UnsafeApiUrlError, WARNING_THRESHOLD, type WatcherOptions, assertWithinBase, buildApiUrl, buildInboundSecurityAdvisory, classifyEmailRoute, closeDatabase, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, flushTelemetry, forgetHostSession, getDatabase, getOperatorEmail, hostSessionStoragePath, isInternalEmail, isSessionFresh, isValidPhoneNumber, loadHostSession, normalizeAddress, normalizePhoneNumber, normalizeSubject, operatorPrefsStoragePath, parseEmail, parseGoogleVoiceSms, recordToolCall, redactObject, redactSecret, resolveConfig, safeJoin, sanitizeEmail, saveConfig, saveHostSession, scanOutboundEmail, scoreEmail, setOperatorEmail, setTelemetryVersion, startRelayBridge, threadIdFor, tryJoin, validateApiUrl };
2627
+ export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentMemoryFields, type AgentMemoryOptions, type AgentMemoryRead, AgentMemoryStore, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, type CachedMessage, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DEFAULT_SESSION_MAX_AGE_MS, DNSConfigurator, type Database, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, type EmailRouteAction, type EmailRouteClass, type EmailRouteClassification, type EmailRouteInput, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type HostName, type HostSession, type InboundEmail, type InboundSmsEvent, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, PathTraversalError, type PurchasedDomain, REDACTED, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, SPAM_THRESHOLD, type SafeJoinOptions, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, type SendSmsInput, type SendSmsResult, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SmsProvider, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, ThreadCache, type ThreadCacheEntry, type ThreadCacheOptions, type ThreadIdInput, type TunnelConfig, TunnelManager, UnsafeApiUrlError, WARNING_THRESHOLD, type WatcherOptions, assertWithinBase, buildApiUrl, buildInboundSecurityAdvisory, classifyEmailRoute, closeDatabase, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, flushTelemetry, forgetHostSession, getDatabase, getOperatorEmail, getSmsProvider, hostSessionStoragePath, isInternalEmail, isSessionFresh, isValidPhoneNumber, loadHostSession, mapProviderSmsStatus, normalizeAddress, normalizePhoneNumber, normalizeSubject, operatorPrefsStoragePath, parseEmail, parseGoogleVoiceSms, recordToolCall, redactObject, redactSecret, redactSmsConfig, resolveConfig, safeJoin, sanitizeEmail, saveConfig, saveHostSession, scanOutboundEmail, scoreEmail, setOperatorEmail, setTelemetryVersion, startRelayBridge, threadIdFor, tryJoin, validateApiUrl };
package/dist/index.d.ts CHANGED
@@ -37,6 +37,12 @@ interface EmailEnvelope {
37
37
  subject: string;
38
38
  from: AddressInfo[];
39
39
  to: AddressInfo[];
40
+ /** Cc/Bcc from the IMAP ENVELOPE response. Added so message-view's
41
+ * quote-audience backfill can pull the previous rounds' Cc/Bcc
42
+ * without a full FETCH BODY round-trip — ENVELOPE already carries
43
+ * them, we just weren't surfacing them. */
44
+ cc?: AddressInfo[];
45
+ bcc?: AddressInfo[];
40
46
  date: Date;
41
47
  flags: Set<string>;
42
48
  size: number;
@@ -1436,13 +1442,13 @@ declare class RelayBridge {
1436
1442
  declare function startRelayBridge(options: RelayBridgeOptions): RelayBridge;
1437
1443
 
1438
1444
  /**
1439
- * SMS Manager - Google Voice SMS integration
1445
+ * SMS Manager - provider-backed SMS integration
1440
1446
  *
1441
1447
  * How it works:
1442
- * 1. User sets up Google Voice with SMS-to-email forwarding
1443
- * 2. Incoming SMS arrives at Google Voice -> forwarded to email -> lands in agent inbox
1444
- * 3. Agent parses forwarded SMS from email body
1445
- * 4. Outgoing SMS sent via Google Voice web interface (browser automation)
1448
+ * 1. User chooses a provider for the agent phone number
1449
+ * 2. Google Voice uses email forwarding/web instructions
1450
+ * 3. 46elks uses direct API sends and inbound webhooks
1451
+ * 4. All inbound/outbound messages are stored in the SMS table
1446
1452
  *
1447
1453
  * SMS config is stored in agent metadata under the "sms" key.
1448
1454
  */
@@ -1450,16 +1456,24 @@ declare function startRelayBridge(options: RelayBridgeOptions): RelayBridge;
1450
1456
  interface SmsConfig {
1451
1457
  /** Whether SMS is enabled for this agent */
1452
1458
  enabled: boolean;
1453
- /** Google Voice phone number (e.g. +12125551234) */
1459
+ /** Phone number in E.164 format where possible */
1454
1460
  phoneNumber: string;
1455
1461
  /** The email address Google Voice forwards SMS to (the Gmail used for GV signup) */
1456
- forwardingEmail: string;
1462
+ forwardingEmail?: string;
1457
1463
  /** App password for forwarding email (only needed if different from relay email) */
1458
1464
  forwardingPassword?: string;
1459
1465
  /** Whether the GV Gmail is the same as the relay email */
1460
1466
  sameAsRelay?: boolean;
1461
- /** Provider (currently only google_voice) */
1462
- provider: 'google_voice';
1467
+ /** SMS provider */
1468
+ provider: 'google_voice' | '46elks';
1469
+ /** 46elks API username */
1470
+ username?: string;
1471
+ /** 46elks API password */
1472
+ password?: string;
1473
+ /** Provider API base URL override */
1474
+ apiUrl?: string;
1475
+ /** Secret required on inbound provider webhooks */
1476
+ webhookSecret?: string;
1463
1477
  /** When SMS was configured */
1464
1478
  configuredAt: string;
1465
1479
  }
@@ -1479,6 +1493,37 @@ interface SmsMessage {
1479
1493
  createdAt: string;
1480
1494
  metadata?: Record<string, unknown>;
1481
1495
  }
1496
+ interface SendSmsInput {
1497
+ to: string;
1498
+ body: string;
1499
+ dryRun?: boolean;
1500
+ }
1501
+ interface SendSmsResult {
1502
+ provider: SmsConfig['provider'];
1503
+ id?: string;
1504
+ status: string;
1505
+ from: string;
1506
+ to: string;
1507
+ body: string;
1508
+ raw?: unknown;
1509
+ }
1510
+ interface InboundSmsEvent {
1511
+ provider: SmsConfig['provider'];
1512
+ id?: string;
1513
+ from: string;
1514
+ to: string;
1515
+ body: string;
1516
+ timestamp: string;
1517
+ raw?: unknown;
1518
+ }
1519
+ interface SmsProvider {
1520
+ id: SmsConfig['provider'];
1521
+ sendSms(config: SmsConfig, input: SendSmsInput): Promise<SendSmsResult>;
1522
+ parseInboundSms(payload: Record<string, unknown>): InboundSmsEvent | null;
1523
+ }
1524
+ declare function redactSmsConfig(config: SmsConfig): SmsConfig;
1525
+ declare function getSmsProvider(provider: SmsConfig['provider']): SmsProvider;
1526
+ declare function mapProviderSmsStatus(status: string): SmsMessage['status'];
1482
1527
  /** Normalize a phone number to E.164-ish format (+1XXXXXXXXXX) */
1483
1528
  declare function normalizePhoneNumber(raw: string): string | null;
1484
1529
  /** Validate a phone number (basic) */
@@ -1500,12 +1545,24 @@ declare function parseGoogleVoiceSms(emailBody: string, emailFrom: string): Pars
1500
1545
  declare function extractVerificationCode(smsBody: string): string | null;
1501
1546
  declare class SmsManager {
1502
1547
  private db;
1548
+ private encryptionKey?;
1503
1549
  private initialized;
1504
- constructor(db: Database);
1550
+ /**
1551
+ * Optional master key used to encrypt SMS credentials at rest (same
1552
+ * AES-256-GCM scheme GatewayManager uses for relay/domain secrets).
1553
+ * When absent (e.g. tests, or a deployment with no master key) configs
1554
+ * are stored as-is and reads tolerate plaintext — so upgrades and
1555
+ * downgrades both stay safe.
1556
+ */
1557
+ constructor(db: Database, encryptionKey?: string | undefined);
1558
+ /** Encrypt the credential fields of an SMS config before persisting. */
1559
+ private encryptConfig;
1560
+ /** Decrypt the credential fields of an SMS config after loading. */
1561
+ private decryptConfig;
1505
1562
  private ensureTable;
1506
- /** Get SMS config from agent metadata */
1563
+ /** Get SMS config from agent metadata (credential fields decrypted). */
1507
1564
  getSmsConfig(agentId: string): SmsConfig | null;
1508
- /** Save SMS config to agent metadata */
1565
+ /** Save SMS config to agent metadata (credential fields encrypted). */
1509
1566
  saveSmsConfig(agentId: string, config: SmsConfig): void;
1510
1567
  /**
1511
1568
  * Resolve the operator's "where do I get pinged" address from an
@@ -1531,12 +1588,17 @@ declare class SmsManager {
1531
1588
  getAlertEmail(agentId: string): string | null;
1532
1589
  /** Remove SMS config from agent metadata */
1533
1590
  removeSmsConfig(agentId: string): void;
1534
- /** Record an inbound SMS (parsed from email) */
1535
- recordInbound(agentId: string, parsed: ParsedSms): SmsMessage;
1591
+ /** Find the agent whose SMS config owns a phone number. */
1592
+ findAgentBySmsNumber(phoneNumber: string, provider?: SmsConfig['provider']): {
1593
+ agentId: string;
1594
+ config: SmsConfig;
1595
+ } | null;
1596
+ /** Record an inbound SMS (parsed from email or provider webhook) */
1597
+ recordInbound(agentId: string, parsed: ParsedSms, metadata?: Record<string, unknown>): SmsMessage;
1536
1598
  /** Record an outbound SMS attempt */
1537
- recordOutbound(agentId: string, phoneNumber: string, body: string, status?: 'pending' | 'sent' | 'failed'): SmsMessage;
1538
- /** Update SMS status */
1539
- updateStatus(id: string, status: SmsMessage['status']): void;
1599
+ recordOutbound(agentId: string, phoneNumber: string, body: string, status?: 'pending' | 'sent' | 'failed', metadata?: Record<string, unknown>): SmsMessage;
1600
+ /** Update SMS status and optional provider metadata */
1601
+ updateStatus(id: string, status: SmsMessage['status'], metadata?: Record<string, unknown>): void;
1540
1602
  /** List SMS messages for an agent */
1541
1603
  listMessages(agentId: string, opts?: {
1542
1604
  direction?: 'inbound' | 'outbound';
@@ -2562,4 +2624,4 @@ declare class AgentMemoryStore {
2562
2624
  renderForPrompt(memory: AgentMemoryRead | null): string;
2563
2625
  }
2564
2626
 
2565
- export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentMemoryFields, type AgentMemoryOptions, type AgentMemoryRead, AgentMemoryStore, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, type CachedMessage, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DEFAULT_SESSION_MAX_AGE_MS, DNSConfigurator, type Database, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, type EmailRouteAction, type EmailRouteClass, type EmailRouteClassification, type EmailRouteInput, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type HostName, type HostSession, type InboundEmail, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, PathTraversalError, type PurchasedDomain, REDACTED, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, SPAM_THRESHOLD, type SafeJoinOptions, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, ThreadCache, type ThreadCacheEntry, type ThreadCacheOptions, type ThreadIdInput, type TunnelConfig, TunnelManager, UnsafeApiUrlError, WARNING_THRESHOLD, type WatcherOptions, assertWithinBase, buildApiUrl, buildInboundSecurityAdvisory, classifyEmailRoute, closeDatabase, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, flushTelemetry, forgetHostSession, getDatabase, getOperatorEmail, hostSessionStoragePath, isInternalEmail, isSessionFresh, isValidPhoneNumber, loadHostSession, normalizeAddress, normalizePhoneNumber, normalizeSubject, operatorPrefsStoragePath, parseEmail, parseGoogleVoiceSms, recordToolCall, redactObject, redactSecret, resolveConfig, safeJoin, sanitizeEmail, saveConfig, saveHostSession, scanOutboundEmail, scoreEmail, setOperatorEmail, setTelemetryVersion, startRelayBridge, threadIdFor, tryJoin, validateApiUrl };
2627
+ export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentMemoryFields, type AgentMemoryOptions, type AgentMemoryRead, AgentMemoryStore, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, type CachedMessage, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DEFAULT_SESSION_MAX_AGE_MS, DNSConfigurator, type Database, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, type EmailRouteAction, type EmailRouteClass, type EmailRouteClassification, type EmailRouteInput, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type HostName, type HostSession, type InboundEmail, type InboundSmsEvent, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, PathTraversalError, type PurchasedDomain, REDACTED, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, SPAM_THRESHOLD, type SafeJoinOptions, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, type SendSmsInput, type SendSmsResult, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SmsProvider, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, ThreadCache, type ThreadCacheEntry, type ThreadCacheOptions, type ThreadIdInput, type TunnelConfig, TunnelManager, UnsafeApiUrlError, WARNING_THRESHOLD, type WatcherOptions, assertWithinBase, buildApiUrl, buildInboundSecurityAdvisory, classifyEmailRoute, closeDatabase, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, flushTelemetry, forgetHostSession, getDatabase, getOperatorEmail, getSmsProvider, hostSessionStoragePath, isInternalEmail, isSessionFresh, isValidPhoneNumber, loadHostSession, mapProviderSmsStatus, normalizeAddress, normalizePhoneNumber, normalizeSubject, operatorPrefsStoragePath, parseEmail, parseGoogleVoiceSms, recordToolCall, redactObject, redactSecret, redactSmsConfig, resolveConfig, safeJoin, sanitizeEmail, saveConfig, saveHostSession, scanOutboundEmail, scoreEmail, setOperatorEmail, setTelemetryVersion, startRelayBridge, threadIdFor, tryJoin, validateApiUrl };
package/dist/index.js CHANGED
@@ -191,6 +191,14 @@ var MailReceiver = class {
191
191
  name: a.name,
192
192
  address: a.address ?? ""
193
193
  })),
194
+ cc: (env.cc ?? []).map((a) => ({
195
+ name: a.name,
196
+ address: a.address ?? ""
197
+ })),
198
+ bcc: (env.bcc ?? []).map((a) => ({
199
+ name: a.name,
200
+ address: a.address ?? ""
201
+ })),
194
202
  date: env.date ?? /* @__PURE__ */ new Date(),
195
203
  flags: msg.flags ?? /* @__PURE__ */ new Set(),
196
204
  size: msg.size ?? 0
@@ -3089,7 +3097,47 @@ var DomainManager = class {
3089
3097
 
3090
3098
  // src/gateway/manager.ts
3091
3099
  import { join as join4 } from "path";
3100
+
3101
+ // src/crypto/secrets.ts
3092
3102
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, createHash, scryptSync } from "crypto";
3103
+ function deriveKey(key, salt) {
3104
+ return scryptSync(key, salt, 32, { N: 16384, r: 8, p: 1 });
3105
+ }
3106
+ function encryptSecret(plaintext, key) {
3107
+ const salt = randomBytes2(16);
3108
+ const derivedKey = deriveKey(key, salt);
3109
+ const iv = randomBytes2(12);
3110
+ const cipher = createCipheriv("aes-256-gcm", derivedKey, iv);
3111
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
3112
+ const authTag = cipher.getAuthTag();
3113
+ return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
3114
+ }
3115
+ function decryptSecret(value, key) {
3116
+ if (value.startsWith("enc2:")) {
3117
+ const parts = value.split(":");
3118
+ if (parts.length !== 5) return value;
3119
+ const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
3120
+ const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
3121
+ const decipher = createDecipheriv("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
3122
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
3123
+ return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
3124
+ }
3125
+ if (value.startsWith("enc:")) {
3126
+ const parts = value.split(":");
3127
+ if (parts.length !== 4) return value;
3128
+ const [, ivHex, authTagHex, ciphertextHex] = parts;
3129
+ const keyHash = createHash("sha256").update(key).digest();
3130
+ const decipher = createDecipheriv("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
3131
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
3132
+ return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
3133
+ }
3134
+ return value;
3135
+ }
3136
+ function isEncryptedSecret(value) {
3137
+ return typeof value === "string" && (value.startsWith("enc2:") || value.startsWith("enc:"));
3138
+ }
3139
+
3140
+ // src/gateway/manager.ts
3093
3141
  import nodemailer3 from "nodemailer";
3094
3142
 
3095
3143
  // src/debug.ts
@@ -4366,6 +4414,130 @@ var TunnelManager = class {
4366
4414
  import MailComposer3 from "nodemailer/lib/mail-composer/index.js";
4367
4415
 
4368
4416
  // src/sms/manager.ts
4417
+ function asString(value) {
4418
+ return typeof value === "string" ? value.trim() : "";
4419
+ }
4420
+ function defaultApiUrl(config) {
4421
+ const url = (config.apiUrl || "https://api.46elks.com/a1").replace(/\/+$/, "");
4422
+ if (!/^https:\/\//i.test(url)) {
4423
+ throw new Error("46elks apiUrl must use https:// \u2014 refusing to send credentials over a non-TLS connection");
4424
+ }
4425
+ return url;
4426
+ }
4427
+ function basicAuth(username, password) {
4428
+ return Buffer.from(`${username}:${password}`, "utf8").toString("base64");
4429
+ }
4430
+ function redactSmsConfig(config) {
4431
+ return {
4432
+ ...config,
4433
+ forwardingPassword: config.forwardingPassword ? "***" : void 0,
4434
+ password: config.password ? "***" : void 0,
4435
+ webhookSecret: config.webhookSecret ? "***" : void 0
4436
+ };
4437
+ }
4438
+ var GoogleVoiceSmsProvider = class {
4439
+ id = "google_voice";
4440
+ async sendSms(config, input) {
4441
+ const to = normalizePhoneNumber(input.to);
4442
+ const from = normalizePhoneNumber(config.phoneNumber);
4443
+ if (!to) throw new Error("Invalid recipient phone number");
4444
+ if (!from) throw new Error("Invalid configured Google Voice phone number");
4445
+ return {
4446
+ provider: this.id,
4447
+ status: "pending",
4448
+ from,
4449
+ to,
4450
+ body: input.body,
4451
+ raw: {
4452
+ delivery: "manual_google_voice_web",
4453
+ url: "https://voice.google.com"
4454
+ }
4455
+ };
4456
+ }
4457
+ parseInboundSms() {
4458
+ return null;
4459
+ }
4460
+ };
4461
+ var FortySixElksSmsProvider = class {
4462
+ id = "46elks";
4463
+ async sendSms(config, input) {
4464
+ const username = asString(config.username);
4465
+ const password = asString(config.password);
4466
+ if (!username || !password) {
4467
+ throw new Error("46elks username and password are required");
4468
+ }
4469
+ const to = normalizePhoneNumber(input.to);
4470
+ const from = normalizePhoneNumber(config.phoneNumber);
4471
+ if (!to) throw new Error("Invalid recipient phone number");
4472
+ if (!from) throw new Error("Invalid configured 46elks phone number");
4473
+ const form = new URLSearchParams();
4474
+ form.set("to", to);
4475
+ form.set("from", from);
4476
+ form.set("message", input.body);
4477
+ if (input.dryRun) form.set("dryrun", "yes");
4478
+ const response = await fetch(`${defaultApiUrl(config)}/sms`, {
4479
+ method: "POST",
4480
+ headers: {
4481
+ "Authorization": `Basic ${basicAuth(username, password)}`,
4482
+ "Content-Type": "application/x-www-form-urlencoded"
4483
+ },
4484
+ body: form,
4485
+ signal: AbortSignal.timeout(15e3)
4486
+ });
4487
+ const text = await response.text();
4488
+ let raw = text;
4489
+ try {
4490
+ raw = JSON.parse(text);
4491
+ } catch {
4492
+ }
4493
+ if (!response.ok) {
4494
+ const message = typeof raw === "object" && raw && ("message" in raw || "error" in raw) ? String(raw.message ?? raw.error) : text.slice(0, 200);
4495
+ throw new Error(`46elks SMS failed (${response.status}): ${message}`);
4496
+ }
4497
+ const providerId = typeof raw === "object" && raw && "id" in raw ? String(raw.id) : void 0;
4498
+ const providerStatus = typeof raw === "object" && raw && "status" in raw ? String(raw.status) : "sent";
4499
+ return {
4500
+ provider: this.id,
4501
+ id: providerId,
4502
+ status: providerStatus,
4503
+ from,
4504
+ to,
4505
+ body: input.body,
4506
+ raw
4507
+ };
4508
+ }
4509
+ parseInboundSms(payload) {
4510
+ const direction = asString(payload.direction).toLowerCase();
4511
+ if (direction && direction !== "incoming") return null;
4512
+ const from = normalizePhoneNumber(asString(payload.from));
4513
+ const to = normalizePhoneNumber(asString(payload.to));
4514
+ const body = asString(payload.message);
4515
+ if (!from || !to || !body) return null;
4516
+ return {
4517
+ provider: this.id,
4518
+ id: asString(payload.id) || void 0,
4519
+ from,
4520
+ to,
4521
+ body,
4522
+ timestamp: asString(payload.created) || (/* @__PURE__ */ new Date()).toISOString(),
4523
+ raw: payload
4524
+ };
4525
+ }
4526
+ };
4527
+ var PROVIDERS = {
4528
+ google_voice: new GoogleVoiceSmsProvider(),
4529
+ "46elks": new FortySixElksSmsProvider()
4530
+ };
4531
+ function getSmsProvider(provider) {
4532
+ return PROVIDERS[provider];
4533
+ }
4534
+ function mapProviderSmsStatus(status) {
4535
+ const normalized = status.toLowerCase();
4536
+ if (normalized === "delivered") return "delivered";
4537
+ if (normalized === "failed" || normalized === "error") return "failed";
4538
+ if (normalized === "created" || normalized === "queued" || normalized === "sent") return "sent";
4539
+ return "sent";
4540
+ }
4369
4541
  function normalizePhoneNumber(raw) {
4370
4542
  const cleaned = raw.replace(/[^+\d]/g, "");
4371
4543
  if (!cleaned) return null;
@@ -4472,12 +4644,48 @@ function extractVerificationCode(smsBody) {
4472
4644
  }
4473
4645
  return null;
4474
4646
  }
4647
+ var SMS_SECRET_FIELDS = ["password", "webhookSecret", "forwardingPassword"];
4475
4648
  var SmsManager = class {
4476
- constructor(db2) {
4649
+ /**
4650
+ * Optional master key used to encrypt SMS credentials at rest (same
4651
+ * AES-256-GCM scheme GatewayManager uses for relay/domain secrets).
4652
+ * When absent (e.g. tests, or a deployment with no master key) configs
4653
+ * are stored as-is and reads tolerate plaintext — so upgrades and
4654
+ * downgrades both stay safe.
4655
+ */
4656
+ constructor(db2, encryptionKey) {
4477
4657
  this.db = db2;
4658
+ this.encryptionKey = encryptionKey;
4478
4659
  this.ensureTable();
4479
4660
  }
4480
4661
  initialized = false;
4662
+ /** Encrypt the credential fields of an SMS config before persisting. */
4663
+ encryptConfig(config) {
4664
+ if (!this.encryptionKey) return config;
4665
+ const out = { ...config };
4666
+ for (const field of SMS_SECRET_FIELDS) {
4667
+ const value = out[field];
4668
+ if (typeof value === "string" && value && !isEncryptedSecret(value)) {
4669
+ out[field] = encryptSecret(value, this.encryptionKey);
4670
+ }
4671
+ }
4672
+ return out;
4673
+ }
4674
+ /** Decrypt the credential fields of an SMS config after loading. */
4675
+ decryptConfig(config) {
4676
+ if (!this.encryptionKey) return config;
4677
+ const out = { ...config };
4678
+ for (const field of SMS_SECRET_FIELDS) {
4679
+ const value = out[field];
4680
+ if (typeof value === "string" && isEncryptedSecret(value)) {
4681
+ try {
4682
+ out[field] = decryptSecret(value, this.encryptionKey);
4683
+ } catch {
4684
+ }
4685
+ }
4686
+ }
4687
+ return out;
4688
+ }
4481
4689
  ensureTable() {
4482
4690
  if (this.initialized) return;
4483
4691
  try {
@@ -4510,18 +4718,19 @@ var SmsManager = class {
4510
4718
  this.initialized = true;
4511
4719
  }
4512
4720
  }
4513
- /** Get SMS config from agent metadata */
4721
+ /** Get SMS config from agent metadata (credential fields decrypted). */
4514
4722
  getSmsConfig(agentId) {
4515
4723
  const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
4516
4724
  if (!row) return null;
4517
4725
  try {
4518
4726
  const meta = JSON.parse(row.metadata || "{}");
4519
- return meta.sms && meta.sms.enabled !== void 0 ? meta.sms : null;
4727
+ if (!meta.sms || meta.sms.enabled === void 0) return null;
4728
+ return this.decryptConfig(meta.sms);
4520
4729
  } catch {
4521
4730
  return null;
4522
4731
  }
4523
4732
  }
4524
- /** Save SMS config to agent metadata */
4733
+ /** Save SMS config to agent metadata (credential fields encrypted). */
4525
4734
  saveSmsConfig(agentId, config) {
4526
4735
  const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
4527
4736
  if (!row) throw new Error(`Agent ${agentId} not found`);
@@ -4531,7 +4740,7 @@ var SmsManager = class {
4531
4740
  } catch {
4532
4741
  meta = {};
4533
4742
  }
4534
- meta.sms = config;
4743
+ meta.sms = this.encryptConfig(config);
4535
4744
  this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
4536
4745
  }
4537
4746
  /**
@@ -4574,27 +4783,50 @@ var SmsManager = class {
4574
4783
  delete meta.sms;
4575
4784
  this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
4576
4785
  }
4577
- /** Record an inbound SMS (parsed from email) */
4578
- recordInbound(agentId, parsed) {
4786
+ /** Find the agent whose SMS config owns a phone number. */
4787
+ findAgentBySmsNumber(phoneNumber, provider) {
4788
+ const normalized = normalizePhoneNumber(phoneNumber);
4789
+ if (!normalized) return null;
4790
+ const rows = this.db.prepare("SELECT id, metadata FROM agents").all();
4791
+ for (const row of rows) {
4792
+ try {
4793
+ const meta = JSON.parse(row.metadata || "{}");
4794
+ const cfg = meta.sms;
4795
+ if (!cfg?.enabled) continue;
4796
+ if (provider && cfg.provider !== provider) continue;
4797
+ if (normalizePhoneNumber(cfg.phoneNumber) === normalized) {
4798
+ return { agentId: row.id, config: this.decryptConfig(cfg) };
4799
+ }
4800
+ } catch {
4801
+ }
4802
+ }
4803
+ return null;
4804
+ }
4805
+ /** Record an inbound SMS (parsed from email or provider webhook) */
4806
+ recordInbound(agentId, parsed, metadata) {
4579
4807
  const id = `sms_in_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
4580
4808
  const createdAt = parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString();
4581
4809
  this.db.prepare(
4582
- "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
4583
- ).run(id, agentId, "inbound", parsed.from, parsed.body, "received", createdAt);
4584
- return { id, agentId, direction: "inbound", phoneNumber: parsed.from, body: parsed.body, status: "received", createdAt };
4810
+ "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
4811
+ ).run(id, agentId, "inbound", parsed.from, parsed.body, "received", createdAt, JSON.stringify(metadata ?? {}));
4812
+ return { id, agentId, direction: "inbound", phoneNumber: parsed.from, body: parsed.body, status: "received", createdAt, metadata };
4585
4813
  }
4586
4814
  /** Record an outbound SMS attempt */
4587
- recordOutbound(agentId, phoneNumber, body, status = "pending") {
4815
+ recordOutbound(agentId, phoneNumber, body, status = "pending", metadata) {
4588
4816
  const id = `sms_out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
4589
4817
  const now = (/* @__PURE__ */ new Date()).toISOString();
4590
4818
  const normalized = normalizePhoneNumber(phoneNumber) || phoneNumber;
4591
4819
  this.db.prepare(
4592
- "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
4593
- ).run(id, agentId, "outbound", normalized, body, status, now);
4594
- return { id, agentId, direction: "outbound", phoneNumber: normalized, body, status, createdAt: now };
4595
- }
4596
- /** Update SMS status */
4597
- updateStatus(id, status) {
4820
+ "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
4821
+ ).run(id, agentId, "outbound", normalized, body, status, now, JSON.stringify(metadata ?? {}));
4822
+ return { id, agentId, direction: "outbound", phoneNumber: normalized, body, status, createdAt: now, metadata };
4823
+ }
4824
+ /** Update SMS status and optional provider metadata */
4825
+ updateStatus(id, status, metadata) {
4826
+ if (metadata) {
4827
+ this.db.prepare("UPDATE sms_messages SET status = ?, metadata = ? WHERE id = ?").run(status, JSON.stringify(metadata), id);
4828
+ return;
4829
+ }
4598
4830
  this.db.prepare("UPDATE sms_messages SET status = ? WHERE id = ?").run(status, id);
4599
4831
  }
4600
4832
  /** List SMS messages for an agent */
@@ -4794,39 +5026,6 @@ var SmsPoller = class {
4794
5026
  };
4795
5027
 
4796
5028
  // src/gateway/manager.ts
4797
- function deriveKey(key, salt) {
4798
- return scryptSync(key, salt, 32, { N: 16384, r: 8, p: 1 });
4799
- }
4800
- function encryptSecret(plaintext, key) {
4801
- const salt = randomBytes2(16);
4802
- const derivedKey = deriveKey(key, salt);
4803
- const iv = randomBytes2(12);
4804
- const cipher = createCipheriv("aes-256-gcm", derivedKey, iv);
4805
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
4806
- const authTag = cipher.getAuthTag();
4807
- return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
4808
- }
4809
- function decryptSecret(value, key) {
4810
- if (value.startsWith("enc2:")) {
4811
- const parts = value.split(":");
4812
- if (parts.length !== 5) return value;
4813
- const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
4814
- const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
4815
- const decipher = createDecipheriv("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
4816
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
4817
- return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
4818
- }
4819
- if (value.startsWith("enc:")) {
4820
- const parts = value.split(":");
4821
- if (parts.length !== 4) return value;
4822
- const [, ivHex, authTagHex, ciphertextHex] = parts;
4823
- const keyHash = createHash("sha256").update(key).digest();
4824
- const decipher = createDecipheriv("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
4825
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
4826
- return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
4827
- }
4828
- return value;
4829
- }
4830
5029
  var GatewayManager = class {
4831
5030
  constructor(options) {
4832
5031
  this.options = options;
@@ -7982,11 +8181,13 @@ export {
7982
8181
  forgetHostSession,
7983
8182
  getDatabase,
7984
8183
  getOperatorEmail,
8184
+ getSmsProvider,
7985
8185
  hostSessionStoragePath,
7986
8186
  isInternalEmail,
7987
8187
  isSessionFresh,
7988
8188
  isValidPhoneNumber,
7989
8189
  loadHostSession,
8190
+ mapProviderSmsStatus,
7990
8191
  normalizeAddress,
7991
8192
  normalizePhoneNumber,
7992
8193
  normalizeSubject,
@@ -7996,6 +8197,7 @@ export {
7996
8197
  recordToolCall,
7997
8198
  redactObject,
7998
8199
  redactSecret,
8200
+ redactSmsConfig,
7999
8201
  resolveConfig,
8000
8202
  safeJoin,
8001
8203
  sanitizeEmail,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "Core SDK for AgenticMail — email, SMS, and phone number access for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",