@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.js
CHANGED
|
@@ -24,8 +24,7 @@ var MailSender = class {
|
|
|
24
24
|
pass: options.password
|
|
25
25
|
},
|
|
26
26
|
tls: {
|
|
27
|
-
rejectUnauthorized:
|
|
28
|
-
// Local dev — no TLS
|
|
27
|
+
rejectUnauthorized: options.tlsRejectUnauthorized ?? true
|
|
29
28
|
},
|
|
30
29
|
connectionTimeout: 1e4,
|
|
31
30
|
// 10s to establish TCP connection
|
|
@@ -3097,7 +3096,47 @@ var DomainManager = class {
|
|
|
3097
3096
|
|
|
3098
3097
|
// src/gateway/manager.ts
|
|
3099
3098
|
import { join as join4 } from "path";
|
|
3099
|
+
|
|
3100
|
+
// src/crypto/secrets.ts
|
|
3100
3101
|
import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, createHash, scryptSync } from "crypto";
|
|
3102
|
+
function deriveKey(key, salt) {
|
|
3103
|
+
return scryptSync(key, salt, 32, { N: 16384, r: 8, p: 1 });
|
|
3104
|
+
}
|
|
3105
|
+
function encryptSecret(plaintext, key) {
|
|
3106
|
+
const salt = randomBytes2(16);
|
|
3107
|
+
const derivedKey = deriveKey(key, salt);
|
|
3108
|
+
const iv = randomBytes2(12);
|
|
3109
|
+
const cipher = createCipheriv("aes-256-gcm", derivedKey, iv);
|
|
3110
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
3111
|
+
const authTag = cipher.getAuthTag();
|
|
3112
|
+
return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
3113
|
+
}
|
|
3114
|
+
function decryptSecret(value, key) {
|
|
3115
|
+
if (value.startsWith("enc2:")) {
|
|
3116
|
+
const parts = value.split(":");
|
|
3117
|
+
if (parts.length !== 5) return value;
|
|
3118
|
+
const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
|
|
3119
|
+
const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
|
|
3120
|
+
const decipher = createDecipheriv("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
|
|
3121
|
+
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
3122
|
+
return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
|
|
3123
|
+
}
|
|
3124
|
+
if (value.startsWith("enc:")) {
|
|
3125
|
+
const parts = value.split(":");
|
|
3126
|
+
if (parts.length !== 4) return value;
|
|
3127
|
+
const [, ivHex, authTagHex, ciphertextHex] = parts;
|
|
3128
|
+
const keyHash = createHash("sha256").update(key).digest();
|
|
3129
|
+
const decipher = createDecipheriv("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
|
|
3130
|
+
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
3131
|
+
return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
|
|
3132
|
+
}
|
|
3133
|
+
return value;
|
|
3134
|
+
}
|
|
3135
|
+
function isEncryptedSecret(value) {
|
|
3136
|
+
return typeof value === "string" && (value.startsWith("enc2:") || value.startsWith("enc:"));
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
// src/gateway/manager.ts
|
|
3101
3140
|
import nodemailer3 from "nodemailer";
|
|
3102
3141
|
|
|
3103
3142
|
// src/debug.ts
|
|
@@ -4374,6 +4413,130 @@ var TunnelManager = class {
|
|
|
4374
4413
|
import MailComposer3 from "nodemailer/lib/mail-composer/index.js";
|
|
4375
4414
|
|
|
4376
4415
|
// src/sms/manager.ts
|
|
4416
|
+
function asString(value) {
|
|
4417
|
+
return typeof value === "string" ? value.trim() : "";
|
|
4418
|
+
}
|
|
4419
|
+
function defaultApiUrl(config) {
|
|
4420
|
+
const url = (config.apiUrl || "https://api.46elks.com/a1").replace(/\/+$/, "");
|
|
4421
|
+
if (!/^https:\/\//i.test(url)) {
|
|
4422
|
+
throw new Error("46elks apiUrl must use https:// \u2014 refusing to send credentials over a non-TLS connection");
|
|
4423
|
+
}
|
|
4424
|
+
return url;
|
|
4425
|
+
}
|
|
4426
|
+
function basicAuth(username, password) {
|
|
4427
|
+
return Buffer.from(`${username}:${password}`, "utf8").toString("base64");
|
|
4428
|
+
}
|
|
4429
|
+
function redactSmsConfig(config) {
|
|
4430
|
+
return {
|
|
4431
|
+
...config,
|
|
4432
|
+
forwardingPassword: config.forwardingPassword ? "***" : void 0,
|
|
4433
|
+
password: config.password ? "***" : void 0,
|
|
4434
|
+
webhookSecret: config.webhookSecret ? "***" : void 0
|
|
4435
|
+
};
|
|
4436
|
+
}
|
|
4437
|
+
var GoogleVoiceSmsProvider = class {
|
|
4438
|
+
id = "google_voice";
|
|
4439
|
+
async sendSms(config, input) {
|
|
4440
|
+
const to = normalizePhoneNumber(input.to);
|
|
4441
|
+
const from = normalizePhoneNumber(config.phoneNumber);
|
|
4442
|
+
if (!to) throw new Error("Invalid recipient phone number");
|
|
4443
|
+
if (!from) throw new Error("Invalid configured Google Voice phone number");
|
|
4444
|
+
return {
|
|
4445
|
+
provider: this.id,
|
|
4446
|
+
status: "pending",
|
|
4447
|
+
from,
|
|
4448
|
+
to,
|
|
4449
|
+
body: input.body,
|
|
4450
|
+
raw: {
|
|
4451
|
+
delivery: "manual_google_voice_web",
|
|
4452
|
+
url: "https://voice.google.com"
|
|
4453
|
+
}
|
|
4454
|
+
};
|
|
4455
|
+
}
|
|
4456
|
+
parseInboundSms() {
|
|
4457
|
+
return null;
|
|
4458
|
+
}
|
|
4459
|
+
};
|
|
4460
|
+
var FortySixElksSmsProvider = class {
|
|
4461
|
+
id = "46elks";
|
|
4462
|
+
async sendSms(config, input) {
|
|
4463
|
+
const username = asString(config.username);
|
|
4464
|
+
const password = asString(config.password);
|
|
4465
|
+
if (!username || !password) {
|
|
4466
|
+
throw new Error("46elks username and password are required");
|
|
4467
|
+
}
|
|
4468
|
+
const to = normalizePhoneNumber(input.to);
|
|
4469
|
+
const from = normalizePhoneNumber(config.phoneNumber);
|
|
4470
|
+
if (!to) throw new Error("Invalid recipient phone number");
|
|
4471
|
+
if (!from) throw new Error("Invalid configured 46elks phone number");
|
|
4472
|
+
const form = new URLSearchParams();
|
|
4473
|
+
form.set("to", to);
|
|
4474
|
+
form.set("from", from);
|
|
4475
|
+
form.set("message", input.body);
|
|
4476
|
+
if (input.dryRun) form.set("dryrun", "yes");
|
|
4477
|
+
const response = await fetch(`${defaultApiUrl(config)}/sms`, {
|
|
4478
|
+
method: "POST",
|
|
4479
|
+
headers: {
|
|
4480
|
+
"Authorization": `Basic ${basicAuth(username, password)}`,
|
|
4481
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
4482
|
+
},
|
|
4483
|
+
body: form,
|
|
4484
|
+
signal: AbortSignal.timeout(15e3)
|
|
4485
|
+
});
|
|
4486
|
+
const text = await response.text();
|
|
4487
|
+
let raw = text;
|
|
4488
|
+
try {
|
|
4489
|
+
raw = JSON.parse(text);
|
|
4490
|
+
} catch {
|
|
4491
|
+
}
|
|
4492
|
+
if (!response.ok) {
|
|
4493
|
+
const message = typeof raw === "object" && raw && ("message" in raw || "error" in raw) ? String(raw.message ?? raw.error) : text.slice(0, 200);
|
|
4494
|
+
throw new Error(`46elks SMS failed (${response.status}): ${message}`);
|
|
4495
|
+
}
|
|
4496
|
+
const providerId = typeof raw === "object" && raw && "id" in raw ? String(raw.id) : void 0;
|
|
4497
|
+
const providerStatus = typeof raw === "object" && raw && "status" in raw ? String(raw.status) : "sent";
|
|
4498
|
+
return {
|
|
4499
|
+
provider: this.id,
|
|
4500
|
+
id: providerId,
|
|
4501
|
+
status: providerStatus,
|
|
4502
|
+
from,
|
|
4503
|
+
to,
|
|
4504
|
+
body: input.body,
|
|
4505
|
+
raw
|
|
4506
|
+
};
|
|
4507
|
+
}
|
|
4508
|
+
parseInboundSms(payload) {
|
|
4509
|
+
const direction = asString(payload.direction).toLowerCase();
|
|
4510
|
+
if (direction && direction !== "incoming") return null;
|
|
4511
|
+
const from = normalizePhoneNumber(asString(payload.from));
|
|
4512
|
+
const to = normalizePhoneNumber(asString(payload.to));
|
|
4513
|
+
const body = asString(payload.message);
|
|
4514
|
+
if (!from || !to || !body) return null;
|
|
4515
|
+
return {
|
|
4516
|
+
provider: this.id,
|
|
4517
|
+
id: asString(payload.id) || void 0,
|
|
4518
|
+
from,
|
|
4519
|
+
to,
|
|
4520
|
+
body,
|
|
4521
|
+
timestamp: asString(payload.created) || (/* @__PURE__ */ new Date()).toISOString(),
|
|
4522
|
+
raw: payload
|
|
4523
|
+
};
|
|
4524
|
+
}
|
|
4525
|
+
};
|
|
4526
|
+
var PROVIDERS = {
|
|
4527
|
+
google_voice: new GoogleVoiceSmsProvider(),
|
|
4528
|
+
"46elks": new FortySixElksSmsProvider()
|
|
4529
|
+
};
|
|
4530
|
+
function getSmsProvider(provider) {
|
|
4531
|
+
return PROVIDERS[provider];
|
|
4532
|
+
}
|
|
4533
|
+
function mapProviderSmsStatus(status) {
|
|
4534
|
+
const normalized = status.toLowerCase();
|
|
4535
|
+
if (normalized === "delivered") return "delivered";
|
|
4536
|
+
if (normalized === "failed" || normalized === "error") return "failed";
|
|
4537
|
+
if (normalized === "created" || normalized === "queued" || normalized === "sent") return "sent";
|
|
4538
|
+
return "sent";
|
|
4539
|
+
}
|
|
4377
4540
|
function normalizePhoneNumber(raw) {
|
|
4378
4541
|
const cleaned = raw.replace(/[^+\d]/g, "");
|
|
4379
4542
|
if (!cleaned) return null;
|
|
@@ -4480,12 +4643,48 @@ function extractVerificationCode(smsBody) {
|
|
|
4480
4643
|
}
|
|
4481
4644
|
return null;
|
|
4482
4645
|
}
|
|
4646
|
+
var SMS_SECRET_FIELDS = ["password", "webhookSecret", "forwardingPassword"];
|
|
4483
4647
|
var SmsManager = class {
|
|
4484
|
-
|
|
4648
|
+
/**
|
|
4649
|
+
* Optional master key used to encrypt SMS credentials at rest (same
|
|
4650
|
+
* AES-256-GCM scheme GatewayManager uses for relay/domain secrets).
|
|
4651
|
+
* When absent (e.g. tests, or a deployment with no master key) configs
|
|
4652
|
+
* are stored as-is and reads tolerate plaintext — so upgrades and
|
|
4653
|
+
* downgrades both stay safe.
|
|
4654
|
+
*/
|
|
4655
|
+
constructor(db2, encryptionKey) {
|
|
4485
4656
|
this.db = db2;
|
|
4657
|
+
this.encryptionKey = encryptionKey;
|
|
4486
4658
|
this.ensureTable();
|
|
4487
4659
|
}
|
|
4488
4660
|
initialized = false;
|
|
4661
|
+
/** Encrypt the credential fields of an SMS config before persisting. */
|
|
4662
|
+
encryptConfig(config) {
|
|
4663
|
+
if (!this.encryptionKey) return config;
|
|
4664
|
+
const out = { ...config };
|
|
4665
|
+
for (const field of SMS_SECRET_FIELDS) {
|
|
4666
|
+
const value = out[field];
|
|
4667
|
+
if (typeof value === "string" && value && !isEncryptedSecret(value)) {
|
|
4668
|
+
out[field] = encryptSecret(value, this.encryptionKey);
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
return out;
|
|
4672
|
+
}
|
|
4673
|
+
/** Decrypt the credential fields of an SMS config after loading. */
|
|
4674
|
+
decryptConfig(config) {
|
|
4675
|
+
if (!this.encryptionKey) return config;
|
|
4676
|
+
const out = { ...config };
|
|
4677
|
+
for (const field of SMS_SECRET_FIELDS) {
|
|
4678
|
+
const value = out[field];
|
|
4679
|
+
if (typeof value === "string" && isEncryptedSecret(value)) {
|
|
4680
|
+
try {
|
|
4681
|
+
out[field] = decryptSecret(value, this.encryptionKey);
|
|
4682
|
+
} catch {
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
return out;
|
|
4687
|
+
}
|
|
4489
4688
|
ensureTable() {
|
|
4490
4689
|
if (this.initialized) return;
|
|
4491
4690
|
try {
|
|
@@ -4518,18 +4717,19 @@ var SmsManager = class {
|
|
|
4518
4717
|
this.initialized = true;
|
|
4519
4718
|
}
|
|
4520
4719
|
}
|
|
4521
|
-
/** Get SMS config from agent metadata */
|
|
4720
|
+
/** Get SMS config from agent metadata (credential fields decrypted). */
|
|
4522
4721
|
getSmsConfig(agentId) {
|
|
4523
4722
|
const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
|
|
4524
4723
|
if (!row) return null;
|
|
4525
4724
|
try {
|
|
4526
4725
|
const meta = JSON.parse(row.metadata || "{}");
|
|
4527
|
-
|
|
4726
|
+
if (!meta.sms || meta.sms.enabled === void 0) return null;
|
|
4727
|
+
return this.decryptConfig(meta.sms);
|
|
4528
4728
|
} catch {
|
|
4529
4729
|
return null;
|
|
4530
4730
|
}
|
|
4531
4731
|
}
|
|
4532
|
-
/** Save SMS config to agent metadata */
|
|
4732
|
+
/** Save SMS config to agent metadata (credential fields encrypted). */
|
|
4533
4733
|
saveSmsConfig(agentId, config) {
|
|
4534
4734
|
const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
|
|
4535
4735
|
if (!row) throw new Error(`Agent ${agentId} not found`);
|
|
@@ -4539,7 +4739,7 @@ var SmsManager = class {
|
|
|
4539
4739
|
} catch {
|
|
4540
4740
|
meta = {};
|
|
4541
4741
|
}
|
|
4542
|
-
meta.sms = config;
|
|
4742
|
+
meta.sms = this.encryptConfig(config);
|
|
4543
4743
|
this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
|
|
4544
4744
|
}
|
|
4545
4745
|
/**
|
|
@@ -4582,27 +4782,50 @@ var SmsManager = class {
|
|
|
4582
4782
|
delete meta.sms;
|
|
4583
4783
|
this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
|
|
4584
4784
|
}
|
|
4585
|
-
/**
|
|
4586
|
-
|
|
4785
|
+
/** Find the agent whose SMS config owns a phone number. */
|
|
4786
|
+
findAgentBySmsNumber(phoneNumber, provider) {
|
|
4787
|
+
const normalized = normalizePhoneNumber(phoneNumber);
|
|
4788
|
+
if (!normalized) return null;
|
|
4789
|
+
const rows = this.db.prepare("SELECT id, metadata FROM agents").all();
|
|
4790
|
+
for (const row of rows) {
|
|
4791
|
+
try {
|
|
4792
|
+
const meta = JSON.parse(row.metadata || "{}");
|
|
4793
|
+
const cfg = meta.sms;
|
|
4794
|
+
if (!cfg?.enabled) continue;
|
|
4795
|
+
if (provider && cfg.provider !== provider) continue;
|
|
4796
|
+
if (normalizePhoneNumber(cfg.phoneNumber) === normalized) {
|
|
4797
|
+
return { agentId: row.id, config: this.decryptConfig(cfg) };
|
|
4798
|
+
}
|
|
4799
|
+
} catch {
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
return null;
|
|
4803
|
+
}
|
|
4804
|
+
/** Record an inbound SMS (parsed from email or provider webhook) */
|
|
4805
|
+
recordInbound(agentId, parsed, metadata) {
|
|
4587
4806
|
const id = `sms_in_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
4588
4807
|
const createdAt = parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
4589
4808
|
this.db.prepare(
|
|
4590
|
-
"INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
4591
|
-
).run(id, agentId, "inbound", parsed.from, parsed.body, "received", createdAt);
|
|
4592
|
-
return { id, agentId, direction: "inbound", phoneNumber: parsed.from, body: parsed.body, status: "received", createdAt };
|
|
4809
|
+
"INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
4810
|
+
).run(id, agentId, "inbound", parsed.from, parsed.body, "received", createdAt, JSON.stringify(metadata ?? {}));
|
|
4811
|
+
return { id, agentId, direction: "inbound", phoneNumber: parsed.from, body: parsed.body, status: "received", createdAt, metadata };
|
|
4593
4812
|
}
|
|
4594
4813
|
/** Record an outbound SMS attempt */
|
|
4595
|
-
recordOutbound(agentId, phoneNumber, body, status = "pending") {
|
|
4814
|
+
recordOutbound(agentId, phoneNumber, body, status = "pending", metadata) {
|
|
4596
4815
|
const id = `sms_out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
4597
4816
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4598
4817
|
const normalized = normalizePhoneNumber(phoneNumber) || phoneNumber;
|
|
4599
4818
|
this.db.prepare(
|
|
4600
|
-
"INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
4601
|
-
).run(id, agentId, "outbound", normalized, body, status, now);
|
|
4602
|
-
return { id, agentId, direction: "outbound", phoneNumber: normalized, body, status, createdAt: now };
|
|
4603
|
-
}
|
|
4604
|
-
/** Update SMS status */
|
|
4605
|
-
updateStatus(id, status) {
|
|
4819
|
+
"INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
4820
|
+
).run(id, agentId, "outbound", normalized, body, status, now, JSON.stringify(metadata ?? {}));
|
|
4821
|
+
return { id, agentId, direction: "outbound", phoneNumber: normalized, body, status, createdAt: now, metadata };
|
|
4822
|
+
}
|
|
4823
|
+
/** Update SMS status and optional provider metadata */
|
|
4824
|
+
updateStatus(id, status, metadata) {
|
|
4825
|
+
if (metadata) {
|
|
4826
|
+
this.db.prepare("UPDATE sms_messages SET status = ?, metadata = ? WHERE id = ?").run(status, JSON.stringify(metadata), id);
|
|
4827
|
+
return;
|
|
4828
|
+
}
|
|
4606
4829
|
this.db.prepare("UPDATE sms_messages SET status = ? WHERE id = ?").run(status, id);
|
|
4607
4830
|
}
|
|
4608
4831
|
/** List SMS messages for an agent */
|
|
@@ -4802,39 +5025,6 @@ var SmsPoller = class {
|
|
|
4802
5025
|
};
|
|
4803
5026
|
|
|
4804
5027
|
// src/gateway/manager.ts
|
|
4805
|
-
function deriveKey(key, salt) {
|
|
4806
|
-
return scryptSync(key, salt, 32, { N: 16384, r: 8, p: 1 });
|
|
4807
|
-
}
|
|
4808
|
-
function encryptSecret(plaintext, key) {
|
|
4809
|
-
const salt = randomBytes2(16);
|
|
4810
|
-
const derivedKey = deriveKey(key, salt);
|
|
4811
|
-
const iv = randomBytes2(12);
|
|
4812
|
-
const cipher = createCipheriv("aes-256-gcm", derivedKey, iv);
|
|
4813
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
4814
|
-
const authTag = cipher.getAuthTag();
|
|
4815
|
-
return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
4816
|
-
}
|
|
4817
|
-
function decryptSecret(value, key) {
|
|
4818
|
-
if (value.startsWith("enc2:")) {
|
|
4819
|
-
const parts = value.split(":");
|
|
4820
|
-
if (parts.length !== 5) return value;
|
|
4821
|
-
const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
|
|
4822
|
-
const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
|
|
4823
|
-
const decipher = createDecipheriv("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
|
|
4824
|
-
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
4825
|
-
return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
|
|
4826
|
-
}
|
|
4827
|
-
if (value.startsWith("enc:")) {
|
|
4828
|
-
const parts = value.split(":");
|
|
4829
|
-
if (parts.length !== 4) return value;
|
|
4830
|
-
const [, ivHex, authTagHex, ciphertextHex] = parts;
|
|
4831
|
-
const keyHash = createHash("sha256").update(key).digest();
|
|
4832
|
-
const decipher = createDecipheriv("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
|
|
4833
|
-
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
4834
|
-
return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
|
|
4835
|
-
}
|
|
4836
|
-
return value;
|
|
4837
|
-
}
|
|
4838
5028
|
var GatewayManager = class {
|
|
4839
5029
|
constructor(options) {
|
|
4840
5030
|
this.options = options;
|
|
@@ -6256,6 +6446,69 @@ function hostSessionStoragePath() {
|
|
|
6256
6446
|
return storagePath();
|
|
6257
6447
|
}
|
|
6258
6448
|
|
|
6449
|
+
// src/host-bridge.ts
|
|
6450
|
+
var BRIDGE_OPERATOR_LIVE_WINDOW_MS = 3e4;
|
|
6451
|
+
var DEFAULT_EXPIRED_MARKERS = [
|
|
6452
|
+
"session not found",
|
|
6453
|
+
"invalid session",
|
|
6454
|
+
"session expired",
|
|
6455
|
+
"no such session",
|
|
6456
|
+
"unknown session",
|
|
6457
|
+
"thread not found",
|
|
6458
|
+
"invalid thread",
|
|
6459
|
+
"thread expired",
|
|
6460
|
+
"no such thread",
|
|
6461
|
+
"unknown thread"
|
|
6462
|
+
];
|
|
6463
|
+
var DEFAULT_SDK_MISSING_MARKERS = [
|
|
6464
|
+
"cannot find module",
|
|
6465
|
+
"could not be found",
|
|
6466
|
+
"command not found"
|
|
6467
|
+
];
|
|
6468
|
+
function bridgeWakeErrorMessage(err) {
|
|
6469
|
+
return err?.message ?? String(err);
|
|
6470
|
+
}
|
|
6471
|
+
function classifyResumeError(err, options = {}) {
|
|
6472
|
+
const msg = bridgeWakeErrorMessage(err).toLowerCase();
|
|
6473
|
+
const expiredMarkers = options.expiredMarkers ?? DEFAULT_EXPIRED_MARKERS;
|
|
6474
|
+
const sdkMissingMarkers = options.sdkMissingMarkers ?? DEFAULT_SDK_MISSING_MARKERS;
|
|
6475
|
+
if (expiredMarkers.some((marker) => msg.includes(marker))) return "session-expired";
|
|
6476
|
+
if (sdkMissingMarkers.some((marker) => msg.includes(marker))) return "sdk-missing";
|
|
6477
|
+
return "other";
|
|
6478
|
+
}
|
|
6479
|
+
function bridgeWakeLastSeenAgeMs(session, nowMs = Date.now()) {
|
|
6480
|
+
if (!session) return null;
|
|
6481
|
+
return nowMs - session.lastSeenMs;
|
|
6482
|
+
}
|
|
6483
|
+
function shouldSkipBridgeWakeForLiveOperator(session, nowMs = Date.now(), liveWindowMs = BRIDGE_OPERATOR_LIVE_WINDOW_MS) {
|
|
6484
|
+
const ageMs = bridgeWakeLastSeenAgeMs(session, nowMs);
|
|
6485
|
+
return ageMs !== null && ageMs < liveWindowMs;
|
|
6486
|
+
}
|
|
6487
|
+
function composeBridgeWakePrompt(args) {
|
|
6488
|
+
const subject = args.subject ?? "(no subject)";
|
|
6489
|
+
const from = args.from ?? "unknown";
|
|
6490
|
+
const preview = (args.preview ?? "").slice(0, 600);
|
|
6491
|
+
return [
|
|
6492
|
+
`\u{1F380} Bridge mail arrived \u2014 headless wake.`,
|
|
6493
|
+
"",
|
|
6494
|
+
`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.`,
|
|
6495
|
+
"",
|
|
6496
|
+
`Trigger:`,
|
|
6497
|
+
` UID: ${args.uid}`,
|
|
6498
|
+
` From: ${from}`,
|
|
6499
|
+
` Subject: ${subject}`,
|
|
6500
|
+
` Preview: ${preview}`,
|
|
6501
|
+
"",
|
|
6502
|
+
`Read it with mcp__agenticmail__read_email({ uid: ${args.uid} }) and decide:`,
|
|
6503
|
+
` \xB7 Does it need a reply from YOU (the operator's session)? Reply via mcp__agenticmail__reply_email.`,
|
|
6504
|
+
` \xB7 Does it need a teammate to act? Forward / re-route by replying with wake: ["<teammate>"].`,
|
|
6505
|
+
` \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.`,
|
|
6506
|
+
` \xB7 Is it FYI noise? mark_read and exit.`,
|
|
6507
|
+
"",
|
|
6508
|
+
`Keep this turn SHORT. You're being resumed to handle ONE piece of mail, not to continue the prior conversation.`
|
|
6509
|
+
].join("\n");
|
|
6510
|
+
}
|
|
6511
|
+
|
|
6259
6512
|
// src/util/safe-url.ts
|
|
6260
6513
|
var UnsafeApiUrlError = class extends Error {
|
|
6261
6514
|
constructor(raw, reason) {
|
|
@@ -7947,6 +8200,7 @@ export {
|
|
|
7947
8200
|
AgentDeletionService,
|
|
7948
8201
|
AgentMemoryStore,
|
|
7949
8202
|
AgenticMailClient,
|
|
8203
|
+
BRIDGE_OPERATOR_LIVE_WINDOW_MS,
|
|
7950
8204
|
CloudflareClient,
|
|
7951
8205
|
DEFAULT_AGENT_NAME,
|
|
7952
8206
|
DEFAULT_AGENT_ROLE,
|
|
@@ -7977,10 +8231,14 @@ export {
|
|
|
7977
8231
|
UnsafeApiUrlError,
|
|
7978
8232
|
WARNING_THRESHOLD,
|
|
7979
8233
|
assertWithinBase,
|
|
8234
|
+
bridgeWakeErrorMessage,
|
|
8235
|
+
bridgeWakeLastSeenAgeMs,
|
|
7980
8236
|
buildApiUrl,
|
|
7981
8237
|
buildInboundSecurityAdvisory,
|
|
7982
8238
|
classifyEmailRoute,
|
|
8239
|
+
classifyResumeError,
|
|
7983
8240
|
closeDatabase,
|
|
8241
|
+
composeBridgeWakePrompt,
|
|
7984
8242
|
createTestDatabase,
|
|
7985
8243
|
debug,
|
|
7986
8244
|
debugWarn,
|
|
@@ -7990,11 +8248,13 @@ export {
|
|
|
7990
8248
|
forgetHostSession,
|
|
7991
8249
|
getDatabase,
|
|
7992
8250
|
getOperatorEmail,
|
|
8251
|
+
getSmsProvider,
|
|
7993
8252
|
hostSessionStoragePath,
|
|
7994
8253
|
isInternalEmail,
|
|
7995
8254
|
isSessionFresh,
|
|
7996
8255
|
isValidPhoneNumber,
|
|
7997
8256
|
loadHostSession,
|
|
8257
|
+
mapProviderSmsStatus,
|
|
7998
8258
|
normalizeAddress,
|
|
7999
8259
|
normalizePhoneNumber,
|
|
8000
8260
|
normalizeSubject,
|
|
@@ -8004,6 +8264,7 @@ export {
|
|
|
8004
8264
|
recordToolCall,
|
|
8005
8265
|
redactObject,
|
|
8006
8266
|
redactSecret,
|
|
8267
|
+
redactSmsConfig,
|
|
8007
8268
|
resolveConfig,
|
|
8008
8269
|
safeJoin,
|
|
8009
8270
|
sanitizeEmail,
|
|
@@ -8013,6 +8274,7 @@ export {
|
|
|
8013
8274
|
scoreEmail,
|
|
8014
8275
|
setOperatorEmail,
|
|
8015
8276
|
setTelemetryVersion,
|
|
8277
|
+
shouldSkipBridgeWakeForLiveOperator,
|
|
8016
8278
|
startRelayBridge,
|
|
8017
8279
|
threadIdFor,
|
|
8018
8280
|
tryJoin,
|