@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 +247 -50
- package/dist/index.d.cts +74 -18
- package/dist/index.d.ts +74 -18
- package/dist/index.js +244 -50
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
5369
|
-
|
|
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 -
|
|
1445
|
+
* SMS Manager - provider-backed SMS integration
|
|
1446
1446
|
*
|
|
1447
1447
|
* How it works:
|
|
1448
|
-
* 1. User
|
|
1449
|
-
* 2.
|
|
1450
|
-
* 3.
|
|
1451
|
-
* 4.
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
1541
|
-
|
|
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 -
|
|
1445
|
+
* SMS Manager - provider-backed SMS integration
|
|
1446
1446
|
*
|
|
1447
1447
|
* How it works:
|
|
1448
|
-
* 1. User
|
|
1449
|
-
* 2.
|
|
1450
|
-
* 3.
|
|
1451
|
-
* 4.
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
1541
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
4586
|
-
|
|
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,
|