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