@agenticmail/core 0.9.8 → 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/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,
@@ -3879,7 +3882,47 @@ var DomainManager = class {
3879
3882
 
3880
3883
  // src/gateway/manager.ts
3881
3884
  var import_node_path4 = require("path");
3885
+
3886
+ // src/crypto/secrets.ts
3882
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
3883
3926
  var import_nodemailer3 = __toESM(require("nodemailer"), 1);
3884
3927
 
3885
3928
  // src/debug.ts
@@ -5157,6 +5200,130 @@ var import_mail_composer3 = __toESM(require("nodemailer/lib/mail-composer/index.
5157
5200
  init_spam_filter();
5158
5201
 
5159
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
+ }
5160
5327
  function normalizePhoneNumber(raw) {
5161
5328
  const cleaned = raw.replace(/[^+\d]/g, "");
5162
5329
  if (!cleaned) return null;
@@ -5263,12 +5430,48 @@ function extractVerificationCode(smsBody) {
5263
5430
  }
5264
5431
  return null;
5265
5432
  }
5433
+ var SMS_SECRET_FIELDS = ["password", "webhookSecret", "forwardingPassword"];
5266
5434
  var SmsManager = class {
5267
- 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) {
5268
5443
  this.db = db2;
5444
+ this.encryptionKey = encryptionKey;
5269
5445
  this.ensureTable();
5270
5446
  }
5271
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
+ }
5272
5475
  ensureTable() {
5273
5476
  if (this.initialized) return;
5274
5477
  try {
@@ -5301,18 +5504,19 @@ var SmsManager = class {
5301
5504
  this.initialized = true;
5302
5505
  }
5303
5506
  }
5304
- /** Get SMS config from agent metadata */
5507
+ /** Get SMS config from agent metadata (credential fields decrypted). */
5305
5508
  getSmsConfig(agentId) {
5306
5509
  const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
5307
5510
  if (!row) return null;
5308
5511
  try {
5309
5512
  const meta = JSON.parse(row.metadata || "{}");
5310
- 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);
5311
5515
  } catch {
5312
5516
  return null;
5313
5517
  }
5314
5518
  }
5315
- /** Save SMS config to agent metadata */
5519
+ /** Save SMS config to agent metadata (credential fields encrypted). */
5316
5520
  saveSmsConfig(agentId, config) {
5317
5521
  const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
5318
5522
  if (!row) throw new Error(`Agent ${agentId} not found`);
@@ -5322,7 +5526,7 @@ var SmsManager = class {
5322
5526
  } catch {
5323
5527
  meta = {};
5324
5528
  }
5325
- meta.sms = config;
5529
+ meta.sms = this.encryptConfig(config);
5326
5530
  this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
5327
5531
  }
5328
5532
  /**
@@ -5365,27 +5569,50 @@ var SmsManager = class {
5365
5569
  delete meta.sms;
5366
5570
  this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
5367
5571
  }
5368
- /** Record an inbound SMS (parsed from email) */
5369
- 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) {
5370
5593
  const id = `sms_in_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
5371
5594
  const createdAt = parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString();
5372
5595
  this.db.prepare(
5373
- "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
5374
- ).run(id, agentId, "inbound", parsed.from, parsed.body, "received", createdAt);
5375
- 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 };
5376
5599
  }
5377
5600
  /** Record an outbound SMS attempt */
5378
- recordOutbound(agentId, phoneNumber, body, status = "pending") {
5601
+ recordOutbound(agentId, phoneNumber, body, status = "pending", metadata) {
5379
5602
  const id = `sms_out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
5380
5603
  const now = (/* @__PURE__ */ new Date()).toISOString();
5381
5604
  const normalized = normalizePhoneNumber(phoneNumber) || phoneNumber;
5382
5605
  this.db.prepare(
5383
- "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
5384
- ).run(id, agentId, "outbound", normalized, body, status, now);
5385
- return { id, agentId, direction: "outbound", phoneNumber: normalized, body, status, createdAt: now };
5386
- }
5387
- /** Update SMS status */
5388
- 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
+ }
5389
5616
  this.db.prepare("UPDATE sms_messages SET status = ? WHERE id = ?").run(status, id);
5390
5617
  }
5391
5618
  /** List SMS messages for an agent */
@@ -5585,39 +5812,6 @@ var SmsPoller = class {
5585
5812
  };
5586
5813
 
5587
5814
  // src/gateway/manager.ts
5588
- function deriveKey(key, salt) {
5589
- return (0, import_node_crypto2.scryptSync)(key, salt, 32, { N: 16384, r: 8, p: 1 });
5590
- }
5591
- function encryptSecret(plaintext, key) {
5592
- const salt = (0, import_node_crypto2.randomBytes)(16);
5593
- const derivedKey = deriveKey(key, salt);
5594
- const iv = (0, import_node_crypto2.randomBytes)(12);
5595
- const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", derivedKey, iv);
5596
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
5597
- const authTag = cipher.getAuthTag();
5598
- return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
5599
- }
5600
- function decryptSecret(value, key) {
5601
- if (value.startsWith("enc2:")) {
5602
- const parts = value.split(":");
5603
- if (parts.length !== 5) return value;
5604
- const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
5605
- const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
5606
- const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
5607
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
5608
- return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
5609
- }
5610
- if (value.startsWith("enc:")) {
5611
- const parts = value.split(":");
5612
- if (parts.length !== 4) return value;
5613
- const [, ivHex, authTagHex, ciphertextHex] = parts;
5614
- const keyHash = (0, import_node_crypto2.createHash)("sha256").update(key).digest();
5615
- const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
5616
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
5617
- return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
5618
- }
5619
- return value;
5620
- }
5621
5815
  var GatewayManager = class {
5622
5816
  constructor(options) {
5623
5817
  this.options = options;
@@ -8759,11 +8953,13 @@ function parse(raw) {
8759
8953
  forgetHostSession,
8760
8954
  getDatabase,
8761
8955
  getOperatorEmail,
8956
+ getSmsProvider,
8762
8957
  hostSessionStoragePath,
8763
8958
  isInternalEmail,
8764
8959
  isSessionFresh,
8765
8960
  isValidPhoneNumber,
8766
8961
  loadHostSession,
8962
+ mapProviderSmsStatus,
8767
8963
  normalizeAddress,
8768
8964
  normalizePhoneNumber,
8769
8965
  normalizeSubject,
@@ -8773,6 +8969,7 @@ function parse(raw) {
8773
8969
  recordToolCall,
8774
8970
  redactObject,
8775
8971
  redactSecret,
8972
+ redactSmsConfig,
8776
8973
  resolveConfig,
8777
8974
  safeJoin,
8778
8975
  sanitizeEmail,
package/dist/index.d.cts CHANGED
@@ -1442,13 +1442,13 @@ declare class RelayBridge {
1442
1442
  declare function startRelayBridge(options: RelayBridgeOptions): RelayBridge;
1443
1443
 
1444
1444
  /**
1445
- * SMS Manager - Google Voice SMS integration
1445
+ * SMS Manager - provider-backed SMS integration
1446
1446
  *
1447
1447
  * How it works:
1448
- * 1. User sets up Google Voice with SMS-to-email forwarding
1449
- * 2. Incoming SMS arrives at Google Voice -> forwarded to email -> lands in agent inbox
1450
- * 3. Agent parses forwarded SMS from email body
1451
- * 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
1452
1452
  *
1453
1453
  * SMS config is stored in agent metadata under the "sms" key.
1454
1454
  */
@@ -1456,16 +1456,24 @@ declare function startRelayBridge(options: RelayBridgeOptions): RelayBridge;
1456
1456
  interface SmsConfig {
1457
1457
  /** Whether SMS is enabled for this agent */
1458
1458
  enabled: boolean;
1459
- /** Google Voice phone number (e.g. +12125551234) */
1459
+ /** Phone number in E.164 format where possible */
1460
1460
  phoneNumber: string;
1461
1461
  /** The email address Google Voice forwards SMS to (the Gmail used for GV signup) */
1462
- forwardingEmail: string;
1462
+ forwardingEmail?: string;
1463
1463
  /** App password for forwarding email (only needed if different from relay email) */
1464
1464
  forwardingPassword?: string;
1465
1465
  /** Whether the GV Gmail is the same as the relay email */
1466
1466
  sameAsRelay?: boolean;
1467
- /** Provider (currently only google_voice) */
1468
- 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;
1469
1477
  /** When SMS was configured */
1470
1478
  configuredAt: string;
1471
1479
  }
@@ -1485,6 +1493,37 @@ interface SmsMessage {
1485
1493
  createdAt: string;
1486
1494
  metadata?: Record<string, unknown>;
1487
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'];
1488
1527
  /** Normalize a phone number to E.164-ish format (+1XXXXXXXXXX) */
1489
1528
  declare function normalizePhoneNumber(raw: string): string | null;
1490
1529
  /** Validate a phone number (basic) */
@@ -1506,12 +1545,24 @@ declare function parseGoogleVoiceSms(emailBody: string, emailFrom: string): Pars
1506
1545
  declare function extractVerificationCode(smsBody: string): string | null;
1507
1546
  declare class SmsManager {
1508
1547
  private db;
1548
+ private encryptionKey?;
1509
1549
  private initialized;
1510
- 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;
1511
1562
  private ensureTable;
1512
- /** Get SMS config from agent metadata */
1563
+ /** Get SMS config from agent metadata (credential fields decrypted). */
1513
1564
  getSmsConfig(agentId: string): SmsConfig | null;
1514
- /** Save SMS config to agent metadata */
1565
+ /** Save SMS config to agent metadata (credential fields encrypted). */
1515
1566
  saveSmsConfig(agentId: string, config: SmsConfig): void;
1516
1567
  /**
1517
1568
  * Resolve the operator's "where do I get pinged" address from an
@@ -1537,12 +1588,17 @@ declare class SmsManager {
1537
1588
  getAlertEmail(agentId: string): string | null;
1538
1589
  /** Remove SMS config from agent metadata */
1539
1590
  removeSmsConfig(agentId: string): void;
1540
- /** Record an inbound SMS (parsed from email) */
1541
- 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;
1542
1598
  /** Record an outbound SMS attempt */
1543
- recordOutbound(agentId: string, phoneNumber: string, body: string, status?: 'pending' | 'sent' | 'failed'): SmsMessage;
1544
- /** Update SMS status */
1545
- 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;
1546
1602
  /** List SMS messages for an agent */
1547
1603
  listMessages(agentId: string, opts?: {
1548
1604
  direction?: 'inbound' | 'outbound';
@@ -2568,4 +2624,4 @@ declare class AgentMemoryStore {
2568
2624
  renderForPrompt(memory: AgentMemoryRead | null): string;
2569
2625
  }
2570
2626
 
2571
- 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
@@ -1442,13 +1442,13 @@ declare class RelayBridge {
1442
1442
  declare function startRelayBridge(options: RelayBridgeOptions): RelayBridge;
1443
1443
 
1444
1444
  /**
1445
- * SMS Manager - Google Voice SMS integration
1445
+ * SMS Manager - provider-backed SMS integration
1446
1446
  *
1447
1447
  * How it works:
1448
- * 1. User sets up Google Voice with SMS-to-email forwarding
1449
- * 2. Incoming SMS arrives at Google Voice -> forwarded to email -> lands in agent inbox
1450
- * 3. Agent parses forwarded SMS from email body
1451
- * 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
1452
1452
  *
1453
1453
  * SMS config is stored in agent metadata under the "sms" key.
1454
1454
  */
@@ -1456,16 +1456,24 @@ declare function startRelayBridge(options: RelayBridgeOptions): RelayBridge;
1456
1456
  interface SmsConfig {
1457
1457
  /** Whether SMS is enabled for this agent */
1458
1458
  enabled: boolean;
1459
- /** Google Voice phone number (e.g. +12125551234) */
1459
+ /** Phone number in E.164 format where possible */
1460
1460
  phoneNumber: string;
1461
1461
  /** The email address Google Voice forwards SMS to (the Gmail used for GV signup) */
1462
- forwardingEmail: string;
1462
+ forwardingEmail?: string;
1463
1463
  /** App password for forwarding email (only needed if different from relay email) */
1464
1464
  forwardingPassword?: string;
1465
1465
  /** Whether the GV Gmail is the same as the relay email */
1466
1466
  sameAsRelay?: boolean;
1467
- /** Provider (currently only google_voice) */
1468
- 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;
1469
1477
  /** When SMS was configured */
1470
1478
  configuredAt: string;
1471
1479
  }
@@ -1485,6 +1493,37 @@ interface SmsMessage {
1485
1493
  createdAt: string;
1486
1494
  metadata?: Record<string, unknown>;
1487
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'];
1488
1527
  /** Normalize a phone number to E.164-ish format (+1XXXXXXXXXX) */
1489
1528
  declare function normalizePhoneNumber(raw: string): string | null;
1490
1529
  /** Validate a phone number (basic) */
@@ -1506,12 +1545,24 @@ declare function parseGoogleVoiceSms(emailBody: string, emailFrom: string): Pars
1506
1545
  declare function extractVerificationCode(smsBody: string): string | null;
1507
1546
  declare class SmsManager {
1508
1547
  private db;
1548
+ private encryptionKey?;
1509
1549
  private initialized;
1510
- 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;
1511
1562
  private ensureTable;
1512
- /** Get SMS config from agent metadata */
1563
+ /** Get SMS config from agent metadata (credential fields decrypted). */
1513
1564
  getSmsConfig(agentId: string): SmsConfig | null;
1514
- /** Save SMS config to agent metadata */
1565
+ /** Save SMS config to agent metadata (credential fields encrypted). */
1515
1566
  saveSmsConfig(agentId: string, config: SmsConfig): void;
1516
1567
  /**
1517
1568
  * Resolve the operator's "where do I get pinged" address from an
@@ -1537,12 +1588,17 @@ declare class SmsManager {
1537
1588
  getAlertEmail(agentId: string): string | null;
1538
1589
  /** Remove SMS config from agent metadata */
1539
1590
  removeSmsConfig(agentId: string): void;
1540
- /** Record an inbound SMS (parsed from email) */
1541
- 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;
1542
1598
  /** Record an outbound SMS attempt */
1543
- recordOutbound(agentId: string, phoneNumber: string, body: string, status?: 'pending' | 'sent' | 'failed'): SmsMessage;
1544
- /** Update SMS status */
1545
- 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;
1546
1602
  /** List SMS messages for an agent */
1547
1603
  listMessages(agentId: string, opts?: {
1548
1604
  direction?: 'inbound' | 'outbound';
@@ -2568,4 +2624,4 @@ declare class AgentMemoryStore {
2568
2624
  renderForPrompt(memory: AgentMemoryRead | null): string;
2569
2625
  }
2570
2626
 
2571
- 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
@@ -3097,7 +3097,47 @@ var DomainManager = class {
3097
3097
 
3098
3098
  // src/gateway/manager.ts
3099
3099
  import { join as join4 } from "path";
3100
+
3101
+ // src/crypto/secrets.ts
3100
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
3101
3141
  import nodemailer3 from "nodemailer";
3102
3142
 
3103
3143
  // src/debug.ts
@@ -4374,6 +4414,130 @@ var TunnelManager = class {
4374
4414
  import MailComposer3 from "nodemailer/lib/mail-composer/index.js";
4375
4415
 
4376
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
+ }
4377
4541
  function normalizePhoneNumber(raw) {
4378
4542
  const cleaned = raw.replace(/[^+\d]/g, "");
4379
4543
  if (!cleaned) return null;
@@ -4480,12 +4644,48 @@ function extractVerificationCode(smsBody) {
4480
4644
  }
4481
4645
  return null;
4482
4646
  }
4647
+ var SMS_SECRET_FIELDS = ["password", "webhookSecret", "forwardingPassword"];
4483
4648
  var SmsManager = class {
4484
- 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) {
4485
4657
  this.db = db2;
4658
+ this.encryptionKey = encryptionKey;
4486
4659
  this.ensureTable();
4487
4660
  }
4488
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
+ }
4489
4689
  ensureTable() {
4490
4690
  if (this.initialized) return;
4491
4691
  try {
@@ -4518,18 +4718,19 @@ var SmsManager = class {
4518
4718
  this.initialized = true;
4519
4719
  }
4520
4720
  }
4521
- /** Get SMS config from agent metadata */
4721
+ /** Get SMS config from agent metadata (credential fields decrypted). */
4522
4722
  getSmsConfig(agentId) {
4523
4723
  const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
4524
4724
  if (!row) return null;
4525
4725
  try {
4526
4726
  const meta = JSON.parse(row.metadata || "{}");
4527
- 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);
4528
4729
  } catch {
4529
4730
  return null;
4530
4731
  }
4531
4732
  }
4532
- /** Save SMS config to agent metadata */
4733
+ /** Save SMS config to agent metadata (credential fields encrypted). */
4533
4734
  saveSmsConfig(agentId, config) {
4534
4735
  const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
4535
4736
  if (!row) throw new Error(`Agent ${agentId} not found`);
@@ -4539,7 +4740,7 @@ var SmsManager = class {
4539
4740
  } catch {
4540
4741
  meta = {};
4541
4742
  }
4542
- meta.sms = config;
4743
+ meta.sms = this.encryptConfig(config);
4543
4744
  this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
4544
4745
  }
4545
4746
  /**
@@ -4582,27 +4783,50 @@ var SmsManager = class {
4582
4783
  delete meta.sms;
4583
4784
  this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
4584
4785
  }
4585
- /** Record an inbound SMS (parsed from email) */
4586
- 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) {
4587
4807
  const id = `sms_in_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
4588
4808
  const createdAt = parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString();
4589
4809
  this.db.prepare(
4590
- "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
4591
- ).run(id, agentId, "inbound", parsed.from, parsed.body, "received", createdAt);
4592
- 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 };
4593
4813
  }
4594
4814
  /** Record an outbound SMS attempt */
4595
- recordOutbound(agentId, phoneNumber, body, status = "pending") {
4815
+ recordOutbound(agentId, phoneNumber, body, status = "pending", metadata) {
4596
4816
  const id = `sms_out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
4597
4817
  const now = (/* @__PURE__ */ new Date()).toISOString();
4598
4818
  const normalized = normalizePhoneNumber(phoneNumber) || phoneNumber;
4599
4819
  this.db.prepare(
4600
- "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
4601
- ).run(id, agentId, "outbound", normalized, body, status, now);
4602
- return { id, agentId, direction: "outbound", phoneNumber: normalized, body, status, createdAt: now };
4603
- }
4604
- /** Update SMS status */
4605
- 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
+ }
4606
4830
  this.db.prepare("UPDATE sms_messages SET status = ? WHERE id = ?").run(status, id);
4607
4831
  }
4608
4832
  /** List SMS messages for an agent */
@@ -4802,39 +5026,6 @@ var SmsPoller = class {
4802
5026
  };
4803
5027
 
4804
5028
  // src/gateway/manager.ts
4805
- function deriveKey(key, salt) {
4806
- return scryptSync(key, salt, 32, { N: 16384, r: 8, p: 1 });
4807
- }
4808
- function encryptSecret(plaintext, key) {
4809
- const salt = randomBytes2(16);
4810
- const derivedKey = deriveKey(key, salt);
4811
- const iv = randomBytes2(12);
4812
- const cipher = createCipheriv("aes-256-gcm", derivedKey, iv);
4813
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
4814
- const authTag = cipher.getAuthTag();
4815
- return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
4816
- }
4817
- function decryptSecret(value, key) {
4818
- if (value.startsWith("enc2:")) {
4819
- const parts = value.split(":");
4820
- if (parts.length !== 5) return value;
4821
- const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
4822
- const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
4823
- const decipher = createDecipheriv("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
4824
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
4825
- return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
4826
- }
4827
- if (value.startsWith("enc:")) {
4828
- const parts = value.split(":");
4829
- if (parts.length !== 4) return value;
4830
- const [, ivHex, authTagHex, ciphertextHex] = parts;
4831
- const keyHash = createHash("sha256").update(key).digest();
4832
- const decipher = createDecipheriv("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
4833
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
4834
- return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
4835
- }
4836
- return value;
4837
- }
4838
5029
  var GatewayManager = class {
4839
5030
  constructor(options) {
4840
5031
  this.options = options;
@@ -7990,11 +8181,13 @@ export {
7990
8181
  forgetHostSession,
7991
8182
  getDatabase,
7992
8183
  getOperatorEmail,
8184
+ getSmsProvider,
7993
8185
  hostSessionStoragePath,
7994
8186
  isInternalEmail,
7995
8187
  isSessionFresh,
7996
8188
  isValidPhoneNumber,
7997
8189
  loadHostSession,
8190
+ mapProviderSmsStatus,
7998
8191
  normalizeAddress,
7999
8192
  normalizePhoneNumber,
8000
8193
  normalizeSubject,
@@ -8004,6 +8197,7 @@ export {
8004
8197
  recordToolCall,
8005
8198
  redactObject,
8006
8199
  redactSecret,
8200
+ redactSmsConfig,
8007
8201
  resolveConfig,
8008
8202
  safeJoin,
8009
8203
  sanitizeEmail,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.9.8",
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",