@agenticmail/core 0.9.8 → 0.9.10
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 +323 -52
- package/dist/index.d.cts +131 -22
- package/dist/index.d.ts +131 -22
- package/dist/index.js +314 -52
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -713,6 +713,7 @@ __export(index_exports, {
|
|
|
713
713
|
AgentDeletionService: () => AgentDeletionService,
|
|
714
714
|
AgentMemoryStore: () => AgentMemoryStore,
|
|
715
715
|
AgenticMailClient: () => AgenticMailClient,
|
|
716
|
+
BRIDGE_OPERATOR_LIVE_WINDOW_MS: () => BRIDGE_OPERATOR_LIVE_WINDOW_MS,
|
|
716
717
|
CloudflareClient: () => CloudflareClient,
|
|
717
718
|
DEFAULT_AGENT_NAME: () => DEFAULT_AGENT_NAME,
|
|
718
719
|
DEFAULT_AGENT_ROLE: () => DEFAULT_AGENT_ROLE,
|
|
@@ -743,10 +744,14 @@ __export(index_exports, {
|
|
|
743
744
|
UnsafeApiUrlError: () => UnsafeApiUrlError,
|
|
744
745
|
WARNING_THRESHOLD: () => WARNING_THRESHOLD,
|
|
745
746
|
assertWithinBase: () => assertWithinBase,
|
|
747
|
+
bridgeWakeErrorMessage: () => bridgeWakeErrorMessage,
|
|
748
|
+
bridgeWakeLastSeenAgeMs: () => bridgeWakeLastSeenAgeMs,
|
|
746
749
|
buildApiUrl: () => buildApiUrl,
|
|
747
750
|
buildInboundSecurityAdvisory: () => buildInboundSecurityAdvisory,
|
|
748
751
|
classifyEmailRoute: () => classifyEmailRoute,
|
|
752
|
+
classifyResumeError: () => classifyResumeError,
|
|
749
753
|
closeDatabase: () => closeDatabase,
|
|
754
|
+
composeBridgeWakePrompt: () => composeBridgeWakePrompt,
|
|
750
755
|
createTestDatabase: () => createTestDatabase,
|
|
751
756
|
debug: () => debug,
|
|
752
757
|
debugWarn: () => debugWarn,
|
|
@@ -756,11 +761,13 @@ __export(index_exports, {
|
|
|
756
761
|
forgetHostSession: () => forgetHostSession,
|
|
757
762
|
getDatabase: () => getDatabase,
|
|
758
763
|
getOperatorEmail: () => getOperatorEmail,
|
|
764
|
+
getSmsProvider: () => getSmsProvider,
|
|
759
765
|
hostSessionStoragePath: () => hostSessionStoragePath,
|
|
760
766
|
isInternalEmail: () => isInternalEmail,
|
|
761
767
|
isSessionFresh: () => isSessionFresh,
|
|
762
768
|
isValidPhoneNumber: () => isValidPhoneNumber,
|
|
763
769
|
loadHostSession: () => loadHostSession,
|
|
770
|
+
mapProviderSmsStatus: () => mapProviderSmsStatus,
|
|
764
771
|
normalizeAddress: () => normalizeAddress,
|
|
765
772
|
normalizePhoneNumber: () => normalizePhoneNumber,
|
|
766
773
|
normalizeSubject: () => normalizeSubject,
|
|
@@ -770,6 +777,7 @@ __export(index_exports, {
|
|
|
770
777
|
recordToolCall: () => recordToolCall,
|
|
771
778
|
redactObject: () => redactObject,
|
|
772
779
|
redactSecret: () => redactSecret,
|
|
780
|
+
redactSmsConfig: () => redactSmsConfig,
|
|
773
781
|
resolveConfig: () => resolveConfig,
|
|
774
782
|
safeJoin: () => safeJoin,
|
|
775
783
|
sanitizeEmail: () => sanitizeEmail,
|
|
@@ -779,6 +787,7 @@ __export(index_exports, {
|
|
|
779
787
|
scoreEmail: () => scoreEmail,
|
|
780
788
|
setOperatorEmail: () => setOperatorEmail,
|
|
781
789
|
setTelemetryVersion: () => setTelemetryVersion,
|
|
790
|
+
shouldSkipBridgeWakeForLiveOperator: () => shouldSkipBridgeWakeForLiveOperator,
|
|
782
791
|
startRelayBridge: () => startRelayBridge,
|
|
783
792
|
threadIdFor: () => threadIdFor,
|
|
784
793
|
tryJoin: () => tryJoin,
|
|
@@ -802,8 +811,7 @@ var MailSender = class {
|
|
|
802
811
|
pass: options.password
|
|
803
812
|
},
|
|
804
813
|
tls: {
|
|
805
|
-
rejectUnauthorized:
|
|
806
|
-
// Local dev — no TLS
|
|
814
|
+
rejectUnauthorized: options.tlsRejectUnauthorized ?? true
|
|
807
815
|
},
|
|
808
816
|
connectionTimeout: 1e4,
|
|
809
817
|
// 10s to establish TCP connection
|
|
@@ -3879,7 +3887,47 @@ var DomainManager = class {
|
|
|
3879
3887
|
|
|
3880
3888
|
// src/gateway/manager.ts
|
|
3881
3889
|
var import_node_path4 = require("path");
|
|
3890
|
+
|
|
3891
|
+
// src/crypto/secrets.ts
|
|
3882
3892
|
var import_node_crypto2 = require("crypto");
|
|
3893
|
+
function deriveKey(key, salt) {
|
|
3894
|
+
return (0, import_node_crypto2.scryptSync)(key, salt, 32, { N: 16384, r: 8, p: 1 });
|
|
3895
|
+
}
|
|
3896
|
+
function encryptSecret(plaintext, key) {
|
|
3897
|
+
const salt = (0, import_node_crypto2.randomBytes)(16);
|
|
3898
|
+
const derivedKey = deriveKey(key, salt);
|
|
3899
|
+
const iv = (0, import_node_crypto2.randomBytes)(12);
|
|
3900
|
+
const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", derivedKey, iv);
|
|
3901
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
3902
|
+
const authTag = cipher.getAuthTag();
|
|
3903
|
+
return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
3904
|
+
}
|
|
3905
|
+
function decryptSecret(value, key) {
|
|
3906
|
+
if (value.startsWith("enc2:")) {
|
|
3907
|
+
const parts = value.split(":");
|
|
3908
|
+
if (parts.length !== 5) return value;
|
|
3909
|
+
const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
|
|
3910
|
+
const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
|
|
3911
|
+
const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
|
|
3912
|
+
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
3913
|
+
return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
|
|
3914
|
+
}
|
|
3915
|
+
if (value.startsWith("enc:")) {
|
|
3916
|
+
const parts = value.split(":");
|
|
3917
|
+
if (parts.length !== 4) return value;
|
|
3918
|
+
const [, ivHex, authTagHex, ciphertextHex] = parts;
|
|
3919
|
+
const keyHash = (0, import_node_crypto2.createHash)("sha256").update(key).digest();
|
|
3920
|
+
const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
|
|
3921
|
+
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
3922
|
+
return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
|
|
3923
|
+
}
|
|
3924
|
+
return value;
|
|
3925
|
+
}
|
|
3926
|
+
function isEncryptedSecret(value) {
|
|
3927
|
+
return typeof value === "string" && (value.startsWith("enc2:") || value.startsWith("enc:"));
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
// src/gateway/manager.ts
|
|
3883
3931
|
var import_nodemailer3 = __toESM(require("nodemailer"), 1);
|
|
3884
3932
|
|
|
3885
3933
|
// src/debug.ts
|
|
@@ -5157,6 +5205,130 @@ var import_mail_composer3 = __toESM(require("nodemailer/lib/mail-composer/index.
|
|
|
5157
5205
|
init_spam_filter();
|
|
5158
5206
|
|
|
5159
5207
|
// src/sms/manager.ts
|
|
5208
|
+
function asString(value) {
|
|
5209
|
+
return typeof value === "string" ? value.trim() : "";
|
|
5210
|
+
}
|
|
5211
|
+
function defaultApiUrl(config) {
|
|
5212
|
+
const url = (config.apiUrl || "https://api.46elks.com/a1").replace(/\/+$/, "");
|
|
5213
|
+
if (!/^https:\/\//i.test(url)) {
|
|
5214
|
+
throw new Error("46elks apiUrl must use https:// \u2014 refusing to send credentials over a non-TLS connection");
|
|
5215
|
+
}
|
|
5216
|
+
return url;
|
|
5217
|
+
}
|
|
5218
|
+
function basicAuth(username, password) {
|
|
5219
|
+
return Buffer.from(`${username}:${password}`, "utf8").toString("base64");
|
|
5220
|
+
}
|
|
5221
|
+
function redactSmsConfig(config) {
|
|
5222
|
+
return {
|
|
5223
|
+
...config,
|
|
5224
|
+
forwardingPassword: config.forwardingPassword ? "***" : void 0,
|
|
5225
|
+
password: config.password ? "***" : void 0,
|
|
5226
|
+
webhookSecret: config.webhookSecret ? "***" : void 0
|
|
5227
|
+
};
|
|
5228
|
+
}
|
|
5229
|
+
var GoogleVoiceSmsProvider = class {
|
|
5230
|
+
id = "google_voice";
|
|
5231
|
+
async sendSms(config, input) {
|
|
5232
|
+
const to = normalizePhoneNumber(input.to);
|
|
5233
|
+
const from = normalizePhoneNumber(config.phoneNumber);
|
|
5234
|
+
if (!to) throw new Error("Invalid recipient phone number");
|
|
5235
|
+
if (!from) throw new Error("Invalid configured Google Voice phone number");
|
|
5236
|
+
return {
|
|
5237
|
+
provider: this.id,
|
|
5238
|
+
status: "pending",
|
|
5239
|
+
from,
|
|
5240
|
+
to,
|
|
5241
|
+
body: input.body,
|
|
5242
|
+
raw: {
|
|
5243
|
+
delivery: "manual_google_voice_web",
|
|
5244
|
+
url: "https://voice.google.com"
|
|
5245
|
+
}
|
|
5246
|
+
};
|
|
5247
|
+
}
|
|
5248
|
+
parseInboundSms() {
|
|
5249
|
+
return null;
|
|
5250
|
+
}
|
|
5251
|
+
};
|
|
5252
|
+
var FortySixElksSmsProvider = class {
|
|
5253
|
+
id = "46elks";
|
|
5254
|
+
async sendSms(config, input) {
|
|
5255
|
+
const username = asString(config.username);
|
|
5256
|
+
const password = asString(config.password);
|
|
5257
|
+
if (!username || !password) {
|
|
5258
|
+
throw new Error("46elks username and password are required");
|
|
5259
|
+
}
|
|
5260
|
+
const to = normalizePhoneNumber(input.to);
|
|
5261
|
+
const from = normalizePhoneNumber(config.phoneNumber);
|
|
5262
|
+
if (!to) throw new Error("Invalid recipient phone number");
|
|
5263
|
+
if (!from) throw new Error("Invalid configured 46elks phone number");
|
|
5264
|
+
const form = new URLSearchParams();
|
|
5265
|
+
form.set("to", to);
|
|
5266
|
+
form.set("from", from);
|
|
5267
|
+
form.set("message", input.body);
|
|
5268
|
+
if (input.dryRun) form.set("dryrun", "yes");
|
|
5269
|
+
const response = await fetch(`${defaultApiUrl(config)}/sms`, {
|
|
5270
|
+
method: "POST",
|
|
5271
|
+
headers: {
|
|
5272
|
+
"Authorization": `Basic ${basicAuth(username, password)}`,
|
|
5273
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
5274
|
+
},
|
|
5275
|
+
body: form,
|
|
5276
|
+
signal: AbortSignal.timeout(15e3)
|
|
5277
|
+
});
|
|
5278
|
+
const text = await response.text();
|
|
5279
|
+
let raw = text;
|
|
5280
|
+
try {
|
|
5281
|
+
raw = JSON.parse(text);
|
|
5282
|
+
} catch {
|
|
5283
|
+
}
|
|
5284
|
+
if (!response.ok) {
|
|
5285
|
+
const message = typeof raw === "object" && raw && ("message" in raw || "error" in raw) ? String(raw.message ?? raw.error) : text.slice(0, 200);
|
|
5286
|
+
throw new Error(`46elks SMS failed (${response.status}): ${message}`);
|
|
5287
|
+
}
|
|
5288
|
+
const providerId = typeof raw === "object" && raw && "id" in raw ? String(raw.id) : void 0;
|
|
5289
|
+
const providerStatus = typeof raw === "object" && raw && "status" in raw ? String(raw.status) : "sent";
|
|
5290
|
+
return {
|
|
5291
|
+
provider: this.id,
|
|
5292
|
+
id: providerId,
|
|
5293
|
+
status: providerStatus,
|
|
5294
|
+
from,
|
|
5295
|
+
to,
|
|
5296
|
+
body: input.body,
|
|
5297
|
+
raw
|
|
5298
|
+
};
|
|
5299
|
+
}
|
|
5300
|
+
parseInboundSms(payload) {
|
|
5301
|
+
const direction = asString(payload.direction).toLowerCase();
|
|
5302
|
+
if (direction && direction !== "incoming") return null;
|
|
5303
|
+
const from = normalizePhoneNumber(asString(payload.from));
|
|
5304
|
+
const to = normalizePhoneNumber(asString(payload.to));
|
|
5305
|
+
const body = asString(payload.message);
|
|
5306
|
+
if (!from || !to || !body) return null;
|
|
5307
|
+
return {
|
|
5308
|
+
provider: this.id,
|
|
5309
|
+
id: asString(payload.id) || void 0,
|
|
5310
|
+
from,
|
|
5311
|
+
to,
|
|
5312
|
+
body,
|
|
5313
|
+
timestamp: asString(payload.created) || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5314
|
+
raw: payload
|
|
5315
|
+
};
|
|
5316
|
+
}
|
|
5317
|
+
};
|
|
5318
|
+
var PROVIDERS = {
|
|
5319
|
+
google_voice: new GoogleVoiceSmsProvider(),
|
|
5320
|
+
"46elks": new FortySixElksSmsProvider()
|
|
5321
|
+
};
|
|
5322
|
+
function getSmsProvider(provider) {
|
|
5323
|
+
return PROVIDERS[provider];
|
|
5324
|
+
}
|
|
5325
|
+
function mapProviderSmsStatus(status) {
|
|
5326
|
+
const normalized = status.toLowerCase();
|
|
5327
|
+
if (normalized === "delivered") return "delivered";
|
|
5328
|
+
if (normalized === "failed" || normalized === "error") return "failed";
|
|
5329
|
+
if (normalized === "created" || normalized === "queued" || normalized === "sent") return "sent";
|
|
5330
|
+
return "sent";
|
|
5331
|
+
}
|
|
5160
5332
|
function normalizePhoneNumber(raw) {
|
|
5161
5333
|
const cleaned = raw.replace(/[^+\d]/g, "");
|
|
5162
5334
|
if (!cleaned) return null;
|
|
@@ -5263,12 +5435,48 @@ function extractVerificationCode(smsBody) {
|
|
|
5263
5435
|
}
|
|
5264
5436
|
return null;
|
|
5265
5437
|
}
|
|
5438
|
+
var SMS_SECRET_FIELDS = ["password", "webhookSecret", "forwardingPassword"];
|
|
5266
5439
|
var SmsManager = class {
|
|
5267
|
-
|
|
5440
|
+
/**
|
|
5441
|
+
* Optional master key used to encrypt SMS credentials at rest (same
|
|
5442
|
+
* AES-256-GCM scheme GatewayManager uses for relay/domain secrets).
|
|
5443
|
+
* When absent (e.g. tests, or a deployment with no master key) configs
|
|
5444
|
+
* are stored as-is and reads tolerate plaintext — so upgrades and
|
|
5445
|
+
* downgrades both stay safe.
|
|
5446
|
+
*/
|
|
5447
|
+
constructor(db2, encryptionKey) {
|
|
5268
5448
|
this.db = db2;
|
|
5449
|
+
this.encryptionKey = encryptionKey;
|
|
5269
5450
|
this.ensureTable();
|
|
5270
5451
|
}
|
|
5271
5452
|
initialized = false;
|
|
5453
|
+
/** Encrypt the credential fields of an SMS config before persisting. */
|
|
5454
|
+
encryptConfig(config) {
|
|
5455
|
+
if (!this.encryptionKey) return config;
|
|
5456
|
+
const out = { ...config };
|
|
5457
|
+
for (const field of SMS_SECRET_FIELDS) {
|
|
5458
|
+
const value = out[field];
|
|
5459
|
+
if (typeof value === "string" && value && !isEncryptedSecret(value)) {
|
|
5460
|
+
out[field] = encryptSecret(value, this.encryptionKey);
|
|
5461
|
+
}
|
|
5462
|
+
}
|
|
5463
|
+
return out;
|
|
5464
|
+
}
|
|
5465
|
+
/** Decrypt the credential fields of an SMS config after loading. */
|
|
5466
|
+
decryptConfig(config) {
|
|
5467
|
+
if (!this.encryptionKey) return config;
|
|
5468
|
+
const out = { ...config };
|
|
5469
|
+
for (const field of SMS_SECRET_FIELDS) {
|
|
5470
|
+
const value = out[field];
|
|
5471
|
+
if (typeof value === "string" && isEncryptedSecret(value)) {
|
|
5472
|
+
try {
|
|
5473
|
+
out[field] = decryptSecret(value, this.encryptionKey);
|
|
5474
|
+
} catch {
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
return out;
|
|
5479
|
+
}
|
|
5272
5480
|
ensureTable() {
|
|
5273
5481
|
if (this.initialized) return;
|
|
5274
5482
|
try {
|
|
@@ -5301,18 +5509,19 @@ var SmsManager = class {
|
|
|
5301
5509
|
this.initialized = true;
|
|
5302
5510
|
}
|
|
5303
5511
|
}
|
|
5304
|
-
/** Get SMS config from agent metadata */
|
|
5512
|
+
/** Get SMS config from agent metadata (credential fields decrypted). */
|
|
5305
5513
|
getSmsConfig(agentId) {
|
|
5306
5514
|
const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
|
|
5307
5515
|
if (!row) return null;
|
|
5308
5516
|
try {
|
|
5309
5517
|
const meta = JSON.parse(row.metadata || "{}");
|
|
5310
|
-
|
|
5518
|
+
if (!meta.sms || meta.sms.enabled === void 0) return null;
|
|
5519
|
+
return this.decryptConfig(meta.sms);
|
|
5311
5520
|
} catch {
|
|
5312
5521
|
return null;
|
|
5313
5522
|
}
|
|
5314
5523
|
}
|
|
5315
|
-
/** Save SMS config to agent metadata */
|
|
5524
|
+
/** Save SMS config to agent metadata (credential fields encrypted). */
|
|
5316
5525
|
saveSmsConfig(agentId, config) {
|
|
5317
5526
|
const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
|
|
5318
5527
|
if (!row) throw new Error(`Agent ${agentId} not found`);
|
|
@@ -5322,7 +5531,7 @@ var SmsManager = class {
|
|
|
5322
5531
|
} catch {
|
|
5323
5532
|
meta = {};
|
|
5324
5533
|
}
|
|
5325
|
-
meta.sms = config;
|
|
5534
|
+
meta.sms = this.encryptConfig(config);
|
|
5326
5535
|
this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
|
|
5327
5536
|
}
|
|
5328
5537
|
/**
|
|
@@ -5365,27 +5574,50 @@ var SmsManager = class {
|
|
|
5365
5574
|
delete meta.sms;
|
|
5366
5575
|
this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
|
|
5367
5576
|
}
|
|
5368
|
-
/**
|
|
5369
|
-
|
|
5577
|
+
/** Find the agent whose SMS config owns a phone number. */
|
|
5578
|
+
findAgentBySmsNumber(phoneNumber, provider) {
|
|
5579
|
+
const normalized = normalizePhoneNumber(phoneNumber);
|
|
5580
|
+
if (!normalized) return null;
|
|
5581
|
+
const rows = this.db.prepare("SELECT id, metadata FROM agents").all();
|
|
5582
|
+
for (const row of rows) {
|
|
5583
|
+
try {
|
|
5584
|
+
const meta = JSON.parse(row.metadata || "{}");
|
|
5585
|
+
const cfg = meta.sms;
|
|
5586
|
+
if (!cfg?.enabled) continue;
|
|
5587
|
+
if (provider && cfg.provider !== provider) continue;
|
|
5588
|
+
if (normalizePhoneNumber(cfg.phoneNumber) === normalized) {
|
|
5589
|
+
return { agentId: row.id, config: this.decryptConfig(cfg) };
|
|
5590
|
+
}
|
|
5591
|
+
} catch {
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
return null;
|
|
5595
|
+
}
|
|
5596
|
+
/** Record an inbound SMS (parsed from email or provider webhook) */
|
|
5597
|
+
recordInbound(agentId, parsed, metadata) {
|
|
5370
5598
|
const id = `sms_in_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5371
5599
|
const createdAt = parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
5372
5600
|
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 };
|
|
5601
|
+
"INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
5602
|
+
).run(id, agentId, "inbound", parsed.from, parsed.body, "received", createdAt, JSON.stringify(metadata ?? {}));
|
|
5603
|
+
return { id, agentId, direction: "inbound", phoneNumber: parsed.from, body: parsed.body, status: "received", createdAt, metadata };
|
|
5376
5604
|
}
|
|
5377
5605
|
/** Record an outbound SMS attempt */
|
|
5378
|
-
recordOutbound(agentId, phoneNumber, body, status = "pending") {
|
|
5606
|
+
recordOutbound(agentId, phoneNumber, body, status = "pending", metadata) {
|
|
5379
5607
|
const id = `sms_out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5380
5608
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5381
5609
|
const normalized = normalizePhoneNumber(phoneNumber) || phoneNumber;
|
|
5382
5610
|
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) {
|
|
5611
|
+
"INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
5612
|
+
).run(id, agentId, "outbound", normalized, body, status, now, JSON.stringify(metadata ?? {}));
|
|
5613
|
+
return { id, agentId, direction: "outbound", phoneNumber: normalized, body, status, createdAt: now, metadata };
|
|
5614
|
+
}
|
|
5615
|
+
/** Update SMS status and optional provider metadata */
|
|
5616
|
+
updateStatus(id, status, metadata) {
|
|
5617
|
+
if (metadata) {
|
|
5618
|
+
this.db.prepare("UPDATE sms_messages SET status = ?, metadata = ? WHERE id = ?").run(status, JSON.stringify(metadata), id);
|
|
5619
|
+
return;
|
|
5620
|
+
}
|
|
5389
5621
|
this.db.prepare("UPDATE sms_messages SET status = ? WHERE id = ?").run(status, id);
|
|
5390
5622
|
}
|
|
5391
5623
|
/** List SMS messages for an agent */
|
|
@@ -5585,39 +5817,6 @@ var SmsPoller = class {
|
|
|
5585
5817
|
};
|
|
5586
5818
|
|
|
5587
5819
|
// 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
5820
|
var GatewayManager = class {
|
|
5622
5821
|
constructor(options) {
|
|
5623
5822
|
this.options = options;
|
|
@@ -7039,6 +7238,69 @@ function hostSessionStoragePath() {
|
|
|
7039
7238
|
return storagePath();
|
|
7040
7239
|
}
|
|
7041
7240
|
|
|
7241
|
+
// src/host-bridge.ts
|
|
7242
|
+
var BRIDGE_OPERATOR_LIVE_WINDOW_MS = 3e4;
|
|
7243
|
+
var DEFAULT_EXPIRED_MARKERS = [
|
|
7244
|
+
"session not found",
|
|
7245
|
+
"invalid session",
|
|
7246
|
+
"session expired",
|
|
7247
|
+
"no such session",
|
|
7248
|
+
"unknown session",
|
|
7249
|
+
"thread not found",
|
|
7250
|
+
"invalid thread",
|
|
7251
|
+
"thread expired",
|
|
7252
|
+
"no such thread",
|
|
7253
|
+
"unknown thread"
|
|
7254
|
+
];
|
|
7255
|
+
var DEFAULT_SDK_MISSING_MARKERS = [
|
|
7256
|
+
"cannot find module",
|
|
7257
|
+
"could not be found",
|
|
7258
|
+
"command not found"
|
|
7259
|
+
];
|
|
7260
|
+
function bridgeWakeErrorMessage(err) {
|
|
7261
|
+
return err?.message ?? String(err);
|
|
7262
|
+
}
|
|
7263
|
+
function classifyResumeError(err, options = {}) {
|
|
7264
|
+
const msg = bridgeWakeErrorMessage(err).toLowerCase();
|
|
7265
|
+
const expiredMarkers = options.expiredMarkers ?? DEFAULT_EXPIRED_MARKERS;
|
|
7266
|
+
const sdkMissingMarkers = options.sdkMissingMarkers ?? DEFAULT_SDK_MISSING_MARKERS;
|
|
7267
|
+
if (expiredMarkers.some((marker) => msg.includes(marker))) return "session-expired";
|
|
7268
|
+
if (sdkMissingMarkers.some((marker) => msg.includes(marker))) return "sdk-missing";
|
|
7269
|
+
return "other";
|
|
7270
|
+
}
|
|
7271
|
+
function bridgeWakeLastSeenAgeMs(session, nowMs = Date.now()) {
|
|
7272
|
+
if (!session) return null;
|
|
7273
|
+
return nowMs - session.lastSeenMs;
|
|
7274
|
+
}
|
|
7275
|
+
function shouldSkipBridgeWakeForLiveOperator(session, nowMs = Date.now(), liveWindowMs = BRIDGE_OPERATOR_LIVE_WINDOW_MS) {
|
|
7276
|
+
const ageMs = bridgeWakeLastSeenAgeMs(session, nowMs);
|
|
7277
|
+
return ageMs !== null && ageMs < liveWindowMs;
|
|
7278
|
+
}
|
|
7279
|
+
function composeBridgeWakePrompt(args) {
|
|
7280
|
+
const subject = args.subject ?? "(no subject)";
|
|
7281
|
+
const from = args.from ?? "unknown";
|
|
7282
|
+
const preview = (args.preview ?? "").slice(0, 600);
|
|
7283
|
+
return [
|
|
7284
|
+
`\u{1F380} Bridge mail arrived \u2014 headless wake.`,
|
|
7285
|
+
"",
|
|
7286
|
+
`You are being resumed against your last session because new mail landed in your bridge inbox (${args.bridgeName}@localhost) and you weren't actively at the keyboard.`,
|
|
7287
|
+
"",
|
|
7288
|
+
`Trigger:`,
|
|
7289
|
+
` UID: ${args.uid}`,
|
|
7290
|
+
` From: ${from}`,
|
|
7291
|
+
` Subject: ${subject}`,
|
|
7292
|
+
` Preview: ${preview}`,
|
|
7293
|
+
"",
|
|
7294
|
+
`Read it with mcp__agenticmail__read_email({ uid: ${args.uid} }) and decide:`,
|
|
7295
|
+
` \xB7 Does it need a reply from YOU (the operator's session)? Reply via mcp__agenticmail__reply_email.`,
|
|
7296
|
+
` \xB7 Does it need a teammate to act? Forward / re-route by replying with wake: ["<teammate>"].`,
|
|
7297
|
+
` \xB7 Is it [NEEDS OPERATOR] / [BLOCKED]? Then it's actually for the human \u2014 mark it unread, and the operator will see it on their next keystroke.`,
|
|
7298
|
+
` \xB7 Is it FYI noise? mark_read and exit.`,
|
|
7299
|
+
"",
|
|
7300
|
+
`Keep this turn SHORT. You're being resumed to handle ONE piece of mail, not to continue the prior conversation.`
|
|
7301
|
+
].join("\n");
|
|
7302
|
+
}
|
|
7303
|
+
|
|
7042
7304
|
// src/util/safe-url.ts
|
|
7043
7305
|
var UnsafeApiUrlError = class extends Error {
|
|
7044
7306
|
constructor(raw, reason) {
|
|
@@ -8716,6 +8978,7 @@ function parse(raw) {
|
|
|
8716
8978
|
AgentDeletionService,
|
|
8717
8979
|
AgentMemoryStore,
|
|
8718
8980
|
AgenticMailClient,
|
|
8981
|
+
BRIDGE_OPERATOR_LIVE_WINDOW_MS,
|
|
8719
8982
|
CloudflareClient,
|
|
8720
8983
|
DEFAULT_AGENT_NAME,
|
|
8721
8984
|
DEFAULT_AGENT_ROLE,
|
|
@@ -8746,10 +9009,14 @@ function parse(raw) {
|
|
|
8746
9009
|
UnsafeApiUrlError,
|
|
8747
9010
|
WARNING_THRESHOLD,
|
|
8748
9011
|
assertWithinBase,
|
|
9012
|
+
bridgeWakeErrorMessage,
|
|
9013
|
+
bridgeWakeLastSeenAgeMs,
|
|
8749
9014
|
buildApiUrl,
|
|
8750
9015
|
buildInboundSecurityAdvisory,
|
|
8751
9016
|
classifyEmailRoute,
|
|
9017
|
+
classifyResumeError,
|
|
8752
9018
|
closeDatabase,
|
|
9019
|
+
composeBridgeWakePrompt,
|
|
8753
9020
|
createTestDatabase,
|
|
8754
9021
|
debug,
|
|
8755
9022
|
debugWarn,
|
|
@@ -8759,11 +9026,13 @@ function parse(raw) {
|
|
|
8759
9026
|
forgetHostSession,
|
|
8760
9027
|
getDatabase,
|
|
8761
9028
|
getOperatorEmail,
|
|
9029
|
+
getSmsProvider,
|
|
8762
9030
|
hostSessionStoragePath,
|
|
8763
9031
|
isInternalEmail,
|
|
8764
9032
|
isSessionFresh,
|
|
8765
9033
|
isValidPhoneNumber,
|
|
8766
9034
|
loadHostSession,
|
|
9035
|
+
mapProviderSmsStatus,
|
|
8767
9036
|
normalizeAddress,
|
|
8768
9037
|
normalizePhoneNumber,
|
|
8769
9038
|
normalizeSubject,
|
|
@@ -8773,6 +9042,7 @@ function parse(raw) {
|
|
|
8773
9042
|
recordToolCall,
|
|
8774
9043
|
redactObject,
|
|
8775
9044
|
redactSecret,
|
|
9045
|
+
redactSmsConfig,
|
|
8776
9046
|
resolveConfig,
|
|
8777
9047
|
safeJoin,
|
|
8778
9048
|
sanitizeEmail,
|
|
@@ -8782,6 +9052,7 @@ function parse(raw) {
|
|
|
8782
9052
|
scoreEmail,
|
|
8783
9053
|
setOperatorEmail,
|
|
8784
9054
|
setTelemetryVersion,
|
|
9055
|
+
shouldSkipBridgeWakeForLiveOperator,
|
|
8785
9056
|
startRelayBridge,
|
|
8786
9057
|
threadIdFor,
|
|
8787
9058
|
tryJoin,
|