@agenticmail/core 0.9.12 → 0.9.14
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 +1041 -8
- package/dist/index.d.cts +304 -1
- package/dist/index.d.ts +304 -1
- package/dist/index.js +1008 -7
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -723,12 +723,26 @@ __export(index_exports, {
|
|
|
723
723
|
DependencyInstaller: () => DependencyInstaller,
|
|
724
724
|
DomainManager: () => DomainManager,
|
|
725
725
|
DomainPurchaser: () => DomainPurchaser,
|
|
726
|
+
ELKS_REALTIME_AUDIO_FORMATS: () => ELKS_REALTIME_AUDIO_FORMATS,
|
|
726
727
|
EmailSearchIndex: () => EmailSearchIndex,
|
|
727
728
|
GatewayManager: () => GatewayManager,
|
|
728
729
|
InboxWatcher: () => InboxWatcher,
|
|
729
730
|
MailReceiver: () => MailReceiver,
|
|
730
731
|
MailSender: () => MailSender,
|
|
732
|
+
PHONE_MAX_CONCURRENT_MISSIONS: () => PHONE_MAX_CONCURRENT_MISSIONS,
|
|
733
|
+
PHONE_MIN_WEBHOOK_SECRET_LENGTH: () => PHONE_MIN_WEBHOOK_SECRET_LENGTH,
|
|
734
|
+
PHONE_MISSION_STATES: () => PHONE_MISSION_STATES,
|
|
735
|
+
PHONE_RATE_LIMIT_PER_HOUR: () => PHONE_RATE_LIMIT_PER_HOUR,
|
|
736
|
+
PHONE_RATE_LIMIT_PER_MINUTE: () => PHONE_RATE_LIMIT_PER_MINUTE,
|
|
737
|
+
PHONE_REGION_SCOPES: () => PHONE_REGION_SCOPES,
|
|
738
|
+
PHONE_SERVER_MAX_ATTEMPTS: () => PHONE_SERVER_MAX_ATTEMPTS,
|
|
739
|
+
PHONE_SERVER_MAX_CALL_DURATION_SECONDS: () => PHONE_SERVER_MAX_CALL_DURATION_SECONDS,
|
|
740
|
+
PHONE_SERVER_MAX_COST_PER_MISSION: () => PHONE_SERVER_MAX_COST_PER_MISSION,
|
|
741
|
+
PHONE_TASK_MAX_LENGTH: () => PHONE_TASK_MAX_LENGTH,
|
|
731
742
|
PathTraversalError: () => PathTraversalError,
|
|
743
|
+
PhoneManager: () => PhoneManager,
|
|
744
|
+
PhoneRateLimitError: () => PhoneRateLimitError,
|
|
745
|
+
PhoneWebhookAuthError: () => PhoneWebhookAuthError,
|
|
732
746
|
REDACTED: () => REDACTED,
|
|
733
747
|
RELAY_PRESETS: () => RELAY_PRESETS,
|
|
734
748
|
RelayBridge: () => RelayBridge,
|
|
@@ -739,6 +753,7 @@ __export(index_exports, {
|
|
|
739
753
|
SmsManager: () => SmsManager,
|
|
740
754
|
SmsPoller: () => SmsPoller,
|
|
741
755
|
StalwartAdmin: () => StalwartAdmin,
|
|
756
|
+
TELEPHONY_TRANSPORT_CAPABILITIES: () => TELEPHONY_TRANSPORT_CAPABILITIES,
|
|
742
757
|
ThreadCache: () => ThreadCache,
|
|
743
758
|
TunnelManager: () => TunnelManager,
|
|
744
759
|
UnsafeApiUrlError: () => UnsafeApiUrlError,
|
|
@@ -747,8 +762,16 @@ __export(index_exports, {
|
|
|
747
762
|
bridgeWakeErrorMessage: () => bridgeWakeErrorMessage,
|
|
748
763
|
bridgeWakeLastSeenAgeMs: () => bridgeWakeLastSeenAgeMs,
|
|
749
764
|
buildApiUrl: () => buildApiUrl,
|
|
765
|
+
buildElksAudioMessage: () => buildElksAudioMessage,
|
|
766
|
+
buildElksByeMessage: () => buildElksByeMessage,
|
|
767
|
+
buildElksHandshakeMessages: () => buildElksHandshakeMessages,
|
|
768
|
+
buildElksInterruptMessage: () => buildElksInterruptMessage,
|
|
769
|
+
buildElksListeningMessage: () => buildElksListeningMessage,
|
|
770
|
+
buildElksSendingMessage: () => buildElksSendingMessage,
|
|
750
771
|
buildInboundSecurityAdvisory: () => buildInboundSecurityAdvisory,
|
|
772
|
+
buildPhoneTransportConfig: () => buildPhoneTransportConfig,
|
|
751
773
|
classifyEmailRoute: () => classifyEmailRoute,
|
|
774
|
+
classifyPhoneNumberRisk: () => classifyPhoneNumberRisk,
|
|
752
775
|
classifyResumeError: () => classifyResumeError,
|
|
753
776
|
closeDatabase: () => closeDatabase,
|
|
754
777
|
composeBridgeWakePrompt: () => composeBridgeWakePrompt,
|
|
@@ -763,7 +786,10 @@ __export(index_exports, {
|
|
|
763
786
|
getOperatorEmail: () => getOperatorEmail,
|
|
764
787
|
getSmsProvider: () => getSmsProvider,
|
|
765
788
|
hostSessionStoragePath: () => hostSessionStoragePath,
|
|
789
|
+
inferPhoneRegion: () => inferPhoneRegion,
|
|
766
790
|
isInternalEmail: () => isInternalEmail,
|
|
791
|
+
isLoopbackMailHost: () => isLoopbackMailHost,
|
|
792
|
+
isPhoneRegionAllowed: () => isPhoneRegionAllowed,
|
|
767
793
|
isSessionFresh: () => isSessionFresh,
|
|
768
794
|
isValidPhoneNumber: () => isValidPhoneNumber,
|
|
769
795
|
loadHostSession: () => loadHostSession,
|
|
@@ -772,14 +798,17 @@ __export(index_exports, {
|
|
|
772
798
|
normalizePhoneNumber: () => normalizePhoneNumber,
|
|
773
799
|
normalizeSubject: () => normalizeSubject,
|
|
774
800
|
operatorPrefsStoragePath: () => operatorPrefsStoragePath,
|
|
801
|
+
parseElksRealtimeMessage: () => parseElksRealtimeMessage,
|
|
775
802
|
parseEmail: () => parseEmail,
|
|
776
803
|
parseGoogleVoiceSms: () => parseGoogleVoiceSms,
|
|
777
804
|
planBridgeWake: () => planBridgeWake,
|
|
778
805
|
recordToolCall: () => recordToolCall,
|
|
779
806
|
redactObject: () => redactObject,
|
|
807
|
+
redactPhoneTransportConfig: () => redactPhoneTransportConfig,
|
|
780
808
|
redactSecret: () => redactSecret,
|
|
781
809
|
redactSmsConfig: () => redactSmsConfig,
|
|
782
810
|
resolveConfig: () => resolveConfig,
|
|
811
|
+
resolveTlsRejectUnauthorized: () => resolveTlsRejectUnauthorized,
|
|
783
812
|
safeJoin: () => safeJoin,
|
|
784
813
|
sanitizeEmail: () => sanitizeEmail,
|
|
785
814
|
saveConfig: () => saveConfig,
|
|
@@ -792,13 +821,24 @@ __export(index_exports, {
|
|
|
792
821
|
startRelayBridge: () => startRelayBridge,
|
|
793
822
|
threadIdFor: () => threadIdFor,
|
|
794
823
|
tryJoin: () => tryJoin,
|
|
795
|
-
validateApiUrl: () => validateApiUrl
|
|
824
|
+
validateApiUrl: () => validateApiUrl,
|
|
825
|
+
validatePhoneMissionPolicy: () => validatePhoneMissionPolicy,
|
|
826
|
+
validatePhoneMissionStart: () => validatePhoneMissionStart,
|
|
827
|
+
validatePhoneTransportProfile: () => validatePhoneTransportProfile
|
|
796
828
|
});
|
|
797
829
|
module.exports = __toCommonJS(index_exports);
|
|
798
830
|
|
|
799
831
|
// src/mail/sender.ts
|
|
800
832
|
var import_nodemailer = __toESM(require("nodemailer"), 1);
|
|
801
833
|
var import_mail_composer = __toESM(require("nodemailer/lib/mail-composer/index.js"), 1);
|
|
834
|
+
function isLoopbackMailHost(host) {
|
|
835
|
+
const h = (host ?? "").trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
836
|
+
return h === "localhost" || h === "::1" || h.endsWith(".localhost") || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
|
|
837
|
+
}
|
|
838
|
+
function resolveTlsRejectUnauthorized(host, explicit) {
|
|
839
|
+
if (explicit !== void 0) return explicit;
|
|
840
|
+
return !isLoopbackMailHost(host);
|
|
841
|
+
}
|
|
802
842
|
var MailSender = class {
|
|
803
843
|
constructor(options) {
|
|
804
844
|
this.options = options;
|
|
@@ -812,7 +852,7 @@ var MailSender = class {
|
|
|
812
852
|
pass: options.password
|
|
813
853
|
},
|
|
814
854
|
tls: {
|
|
815
|
-
rejectUnauthorized: options.tlsRejectUnauthorized
|
|
855
|
+
rejectUnauthorized: resolveTlsRejectUnauthorized(options.host, options.tlsRejectUnauthorized)
|
|
816
856
|
},
|
|
817
857
|
connectionTimeout: 1e4,
|
|
818
858
|
// 10s to establish TCP connection
|
|
@@ -7010,6 +7050,967 @@ var RELAY_PRESETS = {
|
|
|
7010
7050
|
}
|
|
7011
7051
|
};
|
|
7012
7052
|
|
|
7053
|
+
// src/phone/realtime.ts
|
|
7054
|
+
var ELKS_REALTIME_AUDIO_FORMATS = ["ulaw", "pcm_16000", "pcm_24000", "wav"];
|
|
7055
|
+
function asRecord(value) {
|
|
7056
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7057
|
+
}
|
|
7058
|
+
function asString2(value) {
|
|
7059
|
+
return typeof value === "string" ? value.trim() : "";
|
|
7060
|
+
}
|
|
7061
|
+
function isAudioFormat(value) {
|
|
7062
|
+
return typeof value === "string" && ELKS_REALTIME_AUDIO_FORMATS.includes(value);
|
|
7063
|
+
}
|
|
7064
|
+
function assertAudioFormat(format) {
|
|
7065
|
+
if (!isAudioFormat(format)) {
|
|
7066
|
+
throw new Error(`Unsupported 46elks realtime audio format: ${String(format)}`);
|
|
7067
|
+
}
|
|
7068
|
+
return format;
|
|
7069
|
+
}
|
|
7070
|
+
function looksLikeBase64(value) {
|
|
7071
|
+
return value.length > 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value) && value.length % 4 === 0;
|
|
7072
|
+
}
|
|
7073
|
+
function decodeJsonMessage(input) {
|
|
7074
|
+
if (typeof input === "string") {
|
|
7075
|
+
try {
|
|
7076
|
+
return asRecord(JSON.parse(input));
|
|
7077
|
+
} catch {
|
|
7078
|
+
throw new Error("Invalid 46elks realtime message: expected JSON object string");
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
return asRecord(input);
|
|
7082
|
+
}
|
|
7083
|
+
function parseElksRealtimeMessage(input) {
|
|
7084
|
+
const msg = decodeJsonMessage(input);
|
|
7085
|
+
const type = asString2(msg.t);
|
|
7086
|
+
if (type === "hello") {
|
|
7087
|
+
const callid = asString2(msg.callid);
|
|
7088
|
+
const from = asString2(msg.from);
|
|
7089
|
+
const to = asString2(msg.to);
|
|
7090
|
+
if (!callid || !from || !to) {
|
|
7091
|
+
throw new Error("Invalid 46elks realtime hello: callid, from, and to are required");
|
|
7092
|
+
}
|
|
7093
|
+
return { ...msg, t: "hello", callid, from, to };
|
|
7094
|
+
}
|
|
7095
|
+
if (type === "audio") {
|
|
7096
|
+
const data = asString2(msg.data);
|
|
7097
|
+
if (!looksLikeBase64(data)) {
|
|
7098
|
+
throw new Error("Invalid 46elks realtime audio: data must be non-empty base64");
|
|
7099
|
+
}
|
|
7100
|
+
return { t: "audio", data };
|
|
7101
|
+
}
|
|
7102
|
+
if (type === "bye") {
|
|
7103
|
+
const reason = asString2(msg.reason) || void 0;
|
|
7104
|
+
const message = asString2(msg.message) || void 0;
|
|
7105
|
+
return { ...msg, t: "bye", reason, message };
|
|
7106
|
+
}
|
|
7107
|
+
throw new Error(`Unsupported 46elks realtime message type: ${type || "(missing)"}`);
|
|
7108
|
+
}
|
|
7109
|
+
function buildElksListeningMessage(format = "pcm_24000") {
|
|
7110
|
+
return { t: "listening", format: assertAudioFormat(format) };
|
|
7111
|
+
}
|
|
7112
|
+
function buildElksSendingMessage(format = "pcm_24000") {
|
|
7113
|
+
return { t: "sending", format: assertAudioFormat(format) };
|
|
7114
|
+
}
|
|
7115
|
+
function buildElksAudioMessage(data) {
|
|
7116
|
+
const encoded = typeof data === "string" ? data : Buffer.from(data).toString("base64");
|
|
7117
|
+
if (!looksLikeBase64(encoded)) {
|
|
7118
|
+
throw new Error("46elks realtime audio data must be base64 or bytes");
|
|
7119
|
+
}
|
|
7120
|
+
return { t: "audio", data: encoded };
|
|
7121
|
+
}
|
|
7122
|
+
function buildElksInterruptMessage() {
|
|
7123
|
+
return { t: "interrupt" };
|
|
7124
|
+
}
|
|
7125
|
+
function buildElksByeMessage() {
|
|
7126
|
+
return { t: "bye" };
|
|
7127
|
+
}
|
|
7128
|
+
function buildElksHandshakeMessages(options = {}) {
|
|
7129
|
+
return [
|
|
7130
|
+
buildElksListeningMessage(options.listenFormat ?? "pcm_24000"),
|
|
7131
|
+
buildElksSendingMessage(options.sendFormat ?? "pcm_24000")
|
|
7132
|
+
];
|
|
7133
|
+
}
|
|
7134
|
+
|
|
7135
|
+
// src/phone/manager.ts
|
|
7136
|
+
var import_node_crypto3 = require("crypto");
|
|
7137
|
+
|
|
7138
|
+
// src/phone/mission.ts
|
|
7139
|
+
var PHONE_REGION_SCOPES = ["AT", "DE", "EU", "WORLD"];
|
|
7140
|
+
var TELEPHONY_TRANSPORT_CAPABILITIES = [
|
|
7141
|
+
"sms",
|
|
7142
|
+
"call_control",
|
|
7143
|
+
"realtime_media",
|
|
7144
|
+
"recording_supported"
|
|
7145
|
+
];
|
|
7146
|
+
var PHONE_MISSION_STATES = [
|
|
7147
|
+
"draft",
|
|
7148
|
+
"approved",
|
|
7149
|
+
"dialing",
|
|
7150
|
+
"connected",
|
|
7151
|
+
"conversing",
|
|
7152
|
+
"needs_operator",
|
|
7153
|
+
"completed",
|
|
7154
|
+
"failed",
|
|
7155
|
+
"cancelled"
|
|
7156
|
+
];
|
|
7157
|
+
var PHONE_SERVER_MAX_CALL_DURATION_SECONDS = 3600;
|
|
7158
|
+
var PHONE_SERVER_MAX_COST_PER_MISSION = 5;
|
|
7159
|
+
var PHONE_SERVER_MAX_ATTEMPTS = 3;
|
|
7160
|
+
var PHONE_TASK_MAX_LENGTH = 2e3;
|
|
7161
|
+
var EU_DIAL_PREFIXES = [
|
|
7162
|
+
"+30",
|
|
7163
|
+
"+31",
|
|
7164
|
+
"+32",
|
|
7165
|
+
"+33",
|
|
7166
|
+
"+34",
|
|
7167
|
+
"+351",
|
|
7168
|
+
"+352",
|
|
7169
|
+
"+353",
|
|
7170
|
+
"+354",
|
|
7171
|
+
"+356",
|
|
7172
|
+
"+357",
|
|
7173
|
+
"+358",
|
|
7174
|
+
"+359",
|
|
7175
|
+
"+36",
|
|
7176
|
+
"+370",
|
|
7177
|
+
"+371",
|
|
7178
|
+
"+372",
|
|
7179
|
+
"+385",
|
|
7180
|
+
"+386",
|
|
7181
|
+
"+39",
|
|
7182
|
+
"+40",
|
|
7183
|
+
"+420",
|
|
7184
|
+
"+421",
|
|
7185
|
+
"+43",
|
|
7186
|
+
"+45",
|
|
7187
|
+
"+46",
|
|
7188
|
+
"+48",
|
|
7189
|
+
"+49"
|
|
7190
|
+
];
|
|
7191
|
+
var PREMIUM_OR_SPECIAL_PREFIXES = [
|
|
7192
|
+
"+1900",
|
|
7193
|
+
"+1976",
|
|
7194
|
+
"+43810",
|
|
7195
|
+
"+43820",
|
|
7196
|
+
"+43821",
|
|
7197
|
+
"+43828",
|
|
7198
|
+
"+43900",
|
|
7199
|
+
"+43901",
|
|
7200
|
+
"+43930",
|
|
7201
|
+
"+43931",
|
|
7202
|
+
"+49190",
|
|
7203
|
+
"+49900"
|
|
7204
|
+
];
|
|
7205
|
+
function issue(code, field, message) {
|
|
7206
|
+
return { code, field, message };
|
|
7207
|
+
}
|
|
7208
|
+
function isRecord(value) {
|
|
7209
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7210
|
+
}
|
|
7211
|
+
function isPhoneRegionScope(value) {
|
|
7212
|
+
return typeof value === "string" && PHONE_REGION_SCOPES.includes(value);
|
|
7213
|
+
}
|
|
7214
|
+
function isTelephonyTransportCapability(value) {
|
|
7215
|
+
return typeof value === "string" && TELEPHONY_TRANSPORT_CAPABILITIES.includes(value);
|
|
7216
|
+
}
|
|
7217
|
+
function readPositiveInteger(value) {
|
|
7218
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
|
|
7219
|
+
}
|
|
7220
|
+
function readNonNegativeNumber(value) {
|
|
7221
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
7222
|
+
}
|
|
7223
|
+
function readBoolean(value) {
|
|
7224
|
+
return typeof value === "boolean" ? value : null;
|
|
7225
|
+
}
|
|
7226
|
+
function readRegionList(value) {
|
|
7227
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
7228
|
+
const regions = value.filter(isPhoneRegionScope);
|
|
7229
|
+
if (regions.length !== value.length) return null;
|
|
7230
|
+
return Array.from(new Set(regions));
|
|
7231
|
+
}
|
|
7232
|
+
function readCapabilityList(value) {
|
|
7233
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
7234
|
+
const capabilities = value.filter(isTelephonyTransportCapability);
|
|
7235
|
+
if (capabilities.length !== value.length) return null;
|
|
7236
|
+
return Array.from(new Set(capabilities));
|
|
7237
|
+
}
|
|
7238
|
+
function validateConfirmPolicy(value) {
|
|
7239
|
+
const issues = [];
|
|
7240
|
+
if (!isRecord(value)) {
|
|
7241
|
+
return [issue("confirm-policy-required", "policy.confirmPolicy", "confirmPolicy is required")];
|
|
7242
|
+
}
|
|
7243
|
+
const required = {
|
|
7244
|
+
paymentDetails: "never",
|
|
7245
|
+
contractCommitment: "never",
|
|
7246
|
+
costOverLimit: "needs_operator",
|
|
7247
|
+
sensitivePersonalData: "needs_operator",
|
|
7248
|
+
unclearAlternative: "needs_operator"
|
|
7249
|
+
};
|
|
7250
|
+
for (const [field, expected] of Object.entries(required)) {
|
|
7251
|
+
if (value[field] !== expected) {
|
|
7252
|
+
issues.push(issue(
|
|
7253
|
+
"unsafe-confirm-policy",
|
|
7254
|
+
`policy.confirmPolicy.${field}`,
|
|
7255
|
+
`${field} must be ${expected}`
|
|
7256
|
+
));
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7259
|
+
return issues;
|
|
7260
|
+
}
|
|
7261
|
+
function validateAlternativePolicy(value) {
|
|
7262
|
+
if (!isRecord(value)) {
|
|
7263
|
+
return [issue("alternative-policy-required", "policy.alternativePolicy", "alternativePolicy is required")];
|
|
7264
|
+
}
|
|
7265
|
+
const maxTimeShiftMinutes = readNonNegativeNumber(value.maxTimeShiftMinutes);
|
|
7266
|
+
if (maxTimeShiftMinutes === null || !Number.isInteger(maxTimeShiftMinutes)) {
|
|
7267
|
+
return [issue(
|
|
7268
|
+
"invalid-alternative-policy",
|
|
7269
|
+
"policy.alternativePolicy.maxTimeShiftMinutes",
|
|
7270
|
+
"maxTimeShiftMinutes must be a non-negative integer"
|
|
7271
|
+
)];
|
|
7272
|
+
}
|
|
7273
|
+
return [];
|
|
7274
|
+
}
|
|
7275
|
+
function validatePhoneMissionPolicy(policy) {
|
|
7276
|
+
const issues = [];
|
|
7277
|
+
if (!isRecord(policy)) {
|
|
7278
|
+
return { ok: false, issues: [issue("policy-required", "policy", "policy is required")] };
|
|
7279
|
+
}
|
|
7280
|
+
if (policy.policyVersion !== 1) {
|
|
7281
|
+
issues.push(issue("unsupported-policy-version", "policy.policyVersion", "policyVersion must be 1"));
|
|
7282
|
+
}
|
|
7283
|
+
const regionAllowlist = readRegionList(policy.regionAllowlist);
|
|
7284
|
+
if (!regionAllowlist) {
|
|
7285
|
+
issues.push(issue("invalid-region-allowlist", "policy.regionAllowlist", "regionAllowlist must contain at least one supported region"));
|
|
7286
|
+
}
|
|
7287
|
+
const maxCallDurationSeconds = readPositiveInteger(policy.maxCallDurationSeconds);
|
|
7288
|
+
if (maxCallDurationSeconds === null) {
|
|
7289
|
+
issues.push(issue("invalid-max-duration", "policy.maxCallDurationSeconds", "maxCallDurationSeconds must be a positive integer"));
|
|
7290
|
+
}
|
|
7291
|
+
const maxCostPerMission = readNonNegativeNumber(policy.maxCostPerMission);
|
|
7292
|
+
if (maxCostPerMission === null) {
|
|
7293
|
+
issues.push(issue("invalid-max-cost", "policy.maxCostPerMission", "maxCostPerMission must be a non-negative number"));
|
|
7294
|
+
}
|
|
7295
|
+
const maxAttempts = readPositiveInteger(policy.maxAttempts);
|
|
7296
|
+
if (maxAttempts === null) {
|
|
7297
|
+
issues.push(issue("invalid-max-attempts", "policy.maxAttempts", "maxAttempts must be a positive integer"));
|
|
7298
|
+
}
|
|
7299
|
+
const transcriptEnabled = readBoolean(policy.transcriptEnabled);
|
|
7300
|
+
if (transcriptEnabled === null) {
|
|
7301
|
+
issues.push(issue("invalid-transcript-enabled", "policy.transcriptEnabled", "transcriptEnabled must be boolean"));
|
|
7302
|
+
}
|
|
7303
|
+
const recordingEnabled = readBoolean(policy.recordingEnabled);
|
|
7304
|
+
if (recordingEnabled === null) {
|
|
7305
|
+
issues.push(issue("invalid-recording-enabled", "policy.recordingEnabled", "recordingEnabled must be boolean"));
|
|
7306
|
+
}
|
|
7307
|
+
issues.push(...validateConfirmPolicy(policy.confirmPolicy));
|
|
7308
|
+
issues.push(...validateAlternativePolicy(policy.alternativePolicy));
|
|
7309
|
+
if (issues.length > 0) return { ok: false, issues };
|
|
7310
|
+
return {
|
|
7311
|
+
ok: true,
|
|
7312
|
+
policy: {
|
|
7313
|
+
policyVersion: 1,
|
|
7314
|
+
regionAllowlist,
|
|
7315
|
+
maxCallDurationSeconds: Math.min(maxCallDurationSeconds, PHONE_SERVER_MAX_CALL_DURATION_SECONDS),
|
|
7316
|
+
maxCostPerMission: Math.min(maxCostPerMission, PHONE_SERVER_MAX_COST_PER_MISSION),
|
|
7317
|
+
maxAttempts: Math.min(maxAttempts, PHONE_SERVER_MAX_ATTEMPTS),
|
|
7318
|
+
transcriptEnabled,
|
|
7319
|
+
recordingEnabled,
|
|
7320
|
+
confirmPolicy: policy.confirmPolicy,
|
|
7321
|
+
alternativePolicy: {
|
|
7322
|
+
maxTimeShiftMinutes: policy.alternativePolicy.maxTimeShiftMinutes
|
|
7323
|
+
}
|
|
7324
|
+
},
|
|
7325
|
+
issues: []
|
|
7326
|
+
};
|
|
7327
|
+
}
|
|
7328
|
+
function validatePhoneTransportProfile(transport) {
|
|
7329
|
+
const issues = [];
|
|
7330
|
+
if (!isRecord(transport)) {
|
|
7331
|
+
return { ok: false, issues: [issue("transport-required", "transport", "transport profile is required")] };
|
|
7332
|
+
}
|
|
7333
|
+
const provider = typeof transport.provider === "string" ? transport.provider.trim() : "";
|
|
7334
|
+
if (!provider) {
|
|
7335
|
+
issues.push(issue("invalid-provider", "transport.provider", "provider is required"));
|
|
7336
|
+
}
|
|
7337
|
+
const phoneNumber = typeof transport.phoneNumber === "string" ? normalizePhoneNumber(transport.phoneNumber) : null;
|
|
7338
|
+
if (!phoneNumber) {
|
|
7339
|
+
issues.push(issue("invalid-transport-number", "transport.phoneNumber", "transport phoneNumber must be valid E.164"));
|
|
7340
|
+
}
|
|
7341
|
+
const capabilities = readCapabilityList(transport.capabilities);
|
|
7342
|
+
if (!capabilities) {
|
|
7343
|
+
issues.push(issue("invalid-capabilities", "transport.capabilities", "capabilities must contain supported transport capabilities"));
|
|
7344
|
+
} else if (!capabilities.includes("call_control")) {
|
|
7345
|
+
issues.push(issue("missing-call-control", "transport.capabilities", "transport must support call_control to start phone missions"));
|
|
7346
|
+
}
|
|
7347
|
+
const supportedRegions = readRegionList(transport.supportedRegions);
|
|
7348
|
+
if (!supportedRegions) {
|
|
7349
|
+
issues.push(issue("invalid-supported-regions", "transport.supportedRegions", "supportedRegions must contain at least one supported region"));
|
|
7350
|
+
}
|
|
7351
|
+
if (issues.length > 0) return { ok: false, issues };
|
|
7352
|
+
return {
|
|
7353
|
+
ok: true,
|
|
7354
|
+
transport: {
|
|
7355
|
+
provider,
|
|
7356
|
+
phoneNumber,
|
|
7357
|
+
capabilities,
|
|
7358
|
+
supportedRegions
|
|
7359
|
+
},
|
|
7360
|
+
issues: []
|
|
7361
|
+
};
|
|
7362
|
+
}
|
|
7363
|
+
function inferPhoneRegion(phoneNumber) {
|
|
7364
|
+
const normalized = normalizePhoneNumber(phoneNumber);
|
|
7365
|
+
if (!normalized) return null;
|
|
7366
|
+
if (normalized.startsWith("+43")) return "AT";
|
|
7367
|
+
if (normalized.startsWith("+49")) return "DE";
|
|
7368
|
+
if (EU_DIAL_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return "EU";
|
|
7369
|
+
return "WORLD";
|
|
7370
|
+
}
|
|
7371
|
+
function isPhoneRegionAllowed(region, allowlist) {
|
|
7372
|
+
if (allowlist.includes("WORLD")) return true;
|
|
7373
|
+
if (allowlist.includes(region)) return true;
|
|
7374
|
+
if ((region === "AT" || region === "DE" || region === "EU") && allowlist.includes("EU")) return true;
|
|
7375
|
+
return false;
|
|
7376
|
+
}
|
|
7377
|
+
function classifyPhoneNumberRisk(phoneNumber) {
|
|
7378
|
+
const normalized = normalizePhoneNumber(phoneNumber);
|
|
7379
|
+
if (!normalized) return "invalid";
|
|
7380
|
+
if (PREMIUM_OR_SPECIAL_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
|
7381
|
+
return "premium_or_special";
|
|
7382
|
+
}
|
|
7383
|
+
return "standard";
|
|
7384
|
+
}
|
|
7385
|
+
function validatePhoneMissionStart(input, transport, options = {}) {
|
|
7386
|
+
const issues = [];
|
|
7387
|
+
if (!isRecord(input)) {
|
|
7388
|
+
return { ok: false, issues: [issue("start-input-required", "input", "start input is required")] };
|
|
7389
|
+
}
|
|
7390
|
+
const to = typeof input.to === "string" ? normalizePhoneNumber(input.to) : null;
|
|
7391
|
+
if (!to) {
|
|
7392
|
+
issues.push(issue("invalid-target-number", "input.to", "target number must be valid E.164"));
|
|
7393
|
+
}
|
|
7394
|
+
const rawTask = typeof input.task === "string" ? input.task : "";
|
|
7395
|
+
const task = rawTask.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").trim();
|
|
7396
|
+
if (!task) {
|
|
7397
|
+
issues.push(issue("task-required", "input.task", "task is required"));
|
|
7398
|
+
} else if (task.length > PHONE_TASK_MAX_LENGTH) {
|
|
7399
|
+
issues.push(issue("task-too-long", "input.task", `task must be ${PHONE_TASK_MAX_LENGTH} characters or fewer`));
|
|
7400
|
+
}
|
|
7401
|
+
const policyResult = validatePhoneMissionPolicy(input.policy);
|
|
7402
|
+
if (!policyResult.ok) issues.push(...policyResult.issues);
|
|
7403
|
+
const transportResult = validatePhoneTransportProfile(transport);
|
|
7404
|
+
if (!transportResult.ok) issues.push(...transportResult.issues);
|
|
7405
|
+
const risk = typeof input.to === "string" ? classifyPhoneNumberRisk(input.to) : "invalid";
|
|
7406
|
+
if (risk === "premium_or_special" && !options.allowPremiumOrSpecialNumbers) {
|
|
7407
|
+
issues.push(issue("premium-number-blocked", "input.to", "premium or special-rate numbers require an explicit allowlist"));
|
|
7408
|
+
}
|
|
7409
|
+
const targetRegion = to ? inferPhoneRegion(to) : null;
|
|
7410
|
+
if (!targetRegion) {
|
|
7411
|
+
issues.push(issue("unknown-target-region", "input.to", "target region could not be inferred"));
|
|
7412
|
+
}
|
|
7413
|
+
if (policyResult.ok && targetRegion && !isPhoneRegionAllowed(targetRegion, policyResult.policy.regionAllowlist)) {
|
|
7414
|
+
issues.push(issue("region-not-allowed", "input.to", "target number is outside the mission policy regionAllowlist"));
|
|
7415
|
+
}
|
|
7416
|
+
if (transportResult.ok && targetRegion && !isPhoneRegionAllowed(targetRegion, transportResult.transport.supportedRegions)) {
|
|
7417
|
+
issues.push(issue("transport-region-unsupported", "transport.supportedRegions", "target number is outside the transport supportedRegions"));
|
|
7418
|
+
}
|
|
7419
|
+
const capabilities = transportResult.ok ? transportResult.transport.capabilities : [];
|
|
7420
|
+
if (policyResult.ok && policyResult.policy.recordingEnabled && !capabilities.includes("recording_supported")) {
|
|
7421
|
+
issues.push(issue("recording-unsupported", "policy.recordingEnabled", "recordingEnabled requires transport recording_supported capability"));
|
|
7422
|
+
}
|
|
7423
|
+
if (issues.length > 0 || !policyResult.ok || !transportResult.ok || !to || !targetRegion) {
|
|
7424
|
+
return { ok: false, issues };
|
|
7425
|
+
}
|
|
7426
|
+
return {
|
|
7427
|
+
ok: true,
|
|
7428
|
+
mission: {
|
|
7429
|
+
to,
|
|
7430
|
+
task,
|
|
7431
|
+
policy: policyResult.policy,
|
|
7432
|
+
targetRegion,
|
|
7433
|
+
transport: transportResult.transport,
|
|
7434
|
+
voiceRuntimeRef: typeof input.voiceRuntimeRef === "string" && input.voiceRuntimeRef.trim() ? input.voiceRuntimeRef.trim() : void 0
|
|
7435
|
+
},
|
|
7436
|
+
issues: []
|
|
7437
|
+
};
|
|
7438
|
+
}
|
|
7439
|
+
|
|
7440
|
+
// src/phone/manager.ts
|
|
7441
|
+
var PHONE_RATE_LIMIT_PER_MINUTE = 5;
|
|
7442
|
+
var PHONE_RATE_LIMIT_PER_HOUR = 30;
|
|
7443
|
+
var PHONE_MAX_CONCURRENT_MISSIONS = 3;
|
|
7444
|
+
var PHONE_MIN_WEBHOOK_SECRET_LENGTH = 24;
|
|
7445
|
+
var TERMINAL_MISSION_STATES = ["completed", "failed", "cancelled"];
|
|
7446
|
+
var PhoneWebhookAuthError = class extends Error {
|
|
7447
|
+
isPhoneWebhookAuthError = true;
|
|
7448
|
+
constructor() {
|
|
7449
|
+
super("Invalid phone webhook request");
|
|
7450
|
+
this.name = "PhoneWebhookAuthError";
|
|
7451
|
+
}
|
|
7452
|
+
};
|
|
7453
|
+
var PhoneRateLimitError = class extends Error {
|
|
7454
|
+
isPhoneRateLimitError = true;
|
|
7455
|
+
constructor(message) {
|
|
7456
|
+
super(message);
|
|
7457
|
+
this.name = "PhoneRateLimitError";
|
|
7458
|
+
}
|
|
7459
|
+
};
|
|
7460
|
+
var PHONE_SECRET_FIELDS = ["password", "webhookSecret"];
|
|
7461
|
+
var MAX_PHONE_WEBHOOK_EVENT_KEYS = 50;
|
|
7462
|
+
function asString3(value) {
|
|
7463
|
+
return typeof value === "string" ? value.trim() : "";
|
|
7464
|
+
}
|
|
7465
|
+
function asRecord2(value) {
|
|
7466
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7467
|
+
}
|
|
7468
|
+
function defaultApiUrl2(config) {
|
|
7469
|
+
const url = (config.apiUrl || "https://api.46elks.com/a1").replace(/\/+$/, "");
|
|
7470
|
+
if (!/^https:\/\//i.test(url)) {
|
|
7471
|
+
throw new Error("46elks apiUrl must use https:// \u2014 refusing to send credentials over a non-TLS connection");
|
|
7472
|
+
}
|
|
7473
|
+
return url;
|
|
7474
|
+
}
|
|
7475
|
+
function basicAuth2(username, password) {
|
|
7476
|
+
return Buffer.from(`${username}:${password}`, "utf8").toString("base64");
|
|
7477
|
+
}
|
|
7478
|
+
function secretMatches(provided, expected) {
|
|
7479
|
+
const a = Buffer.from(provided);
|
|
7480
|
+
const b = Buffer.from(expected);
|
|
7481
|
+
return a.length === b.length && (0, import_node_crypto3.timingSafeEqual)(a, b);
|
|
7482
|
+
}
|
|
7483
|
+
function apiBaseUrl(webhookBaseUrl) {
|
|
7484
|
+
const root = webhookBaseUrl.replace(/\/+$/, "");
|
|
7485
|
+
return root.endsWith("/api/agenticmail") ? root : `${root}/api/agenticmail`;
|
|
7486
|
+
}
|
|
7487
|
+
function webhookToken(webhookSecret, missionId) {
|
|
7488
|
+
return (0, import_node_crypto3.createHmac)("sha256", webhookSecret).update(missionId).digest("hex");
|
|
7489
|
+
}
|
|
7490
|
+
function buildWebhookUrl(config, path2, missionId) {
|
|
7491
|
+
const url = new URL(`${apiBaseUrl(config.webhookBaseUrl)}${path2}`);
|
|
7492
|
+
url.searchParams.set("missionId", missionId);
|
|
7493
|
+
url.searchParams.set("token", webhookToken(config.webhookSecret, missionId));
|
|
7494
|
+
return url.toString();
|
|
7495
|
+
}
|
|
7496
|
+
function redactWebhookUrl(value) {
|
|
7497
|
+
try {
|
|
7498
|
+
const url = new URL(value);
|
|
7499
|
+
if (url.searchParams.has("token")) url.searchParams.set("token", "***");
|
|
7500
|
+
if (url.searchParams.has("secret")) url.searchParams.set("secret", "***");
|
|
7501
|
+
return url.toString();
|
|
7502
|
+
} catch {
|
|
7503
|
+
return "[redacted-url]";
|
|
7504
|
+
}
|
|
7505
|
+
}
|
|
7506
|
+
function redactProviderRequest(request) {
|
|
7507
|
+
return {
|
|
7508
|
+
url: request.url,
|
|
7509
|
+
body: {
|
|
7510
|
+
...request.body,
|
|
7511
|
+
voice_start: redactWebhookUrl(request.body.voice_start),
|
|
7512
|
+
whenhangup: redactWebhookUrl(request.body.whenhangup)
|
|
7513
|
+
}
|
|
7514
|
+
};
|
|
7515
|
+
}
|
|
7516
|
+
function stableFlatJson(value) {
|
|
7517
|
+
return JSON.stringify(Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))));
|
|
7518
|
+
}
|
|
7519
|
+
function phoneWebhookEventKey(kind, payload) {
|
|
7520
|
+
const callId = asString3(payload.callid) || asString3(payload.id) || asString3(payload.call_id);
|
|
7521
|
+
const result = asString3(payload.result) || asString3(payload.status) || asString3(payload.why);
|
|
7522
|
+
const fingerprint = (0, import_node_crypto3.createHash)("sha256").update(stableFlatJson(payload)).digest("hex").slice(0, 16);
|
|
7523
|
+
return [kind, callId || fingerprint, result].filter(Boolean).join(":");
|
|
7524
|
+
}
|
|
7525
|
+
function processedWebhookEventKeys(mission) {
|
|
7526
|
+
const value = mission.metadata.phoneWebhookEvents;
|
|
7527
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
7528
|
+
}
|
|
7529
|
+
function hasProcessedWebhookEvent(mission, eventKey) {
|
|
7530
|
+
return processedWebhookEventKeys(mission).includes(eventKey);
|
|
7531
|
+
}
|
|
7532
|
+
function appendProcessedWebhookEvent(mission, eventKey) {
|
|
7533
|
+
return [...processedWebhookEventKeys(mission), eventKey].slice(-MAX_PHONE_WEBHOOK_EVENT_KEYS);
|
|
7534
|
+
}
|
|
7535
|
+
function parseJson(value, fallback) {
|
|
7536
|
+
if (!value) return fallback;
|
|
7537
|
+
try {
|
|
7538
|
+
return JSON.parse(value);
|
|
7539
|
+
} catch {
|
|
7540
|
+
return fallback;
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7543
|
+
function rowToMission(row) {
|
|
7544
|
+
return {
|
|
7545
|
+
id: row.id,
|
|
7546
|
+
agentId: row.agent_id,
|
|
7547
|
+
status: row.status,
|
|
7548
|
+
from: row.from_phone,
|
|
7549
|
+
to: row.to_phone,
|
|
7550
|
+
task: row.task,
|
|
7551
|
+
policy: parseJson(row.policy_json, {}),
|
|
7552
|
+
transport: parseJson(row.transport_json, {}),
|
|
7553
|
+
provider: row.provider,
|
|
7554
|
+
providerCallId: row.provider_call_id ?? void 0,
|
|
7555
|
+
transcript: parseJson(row.transcript_json, []),
|
|
7556
|
+
metadata: parseJson(row.metadata_json, {}),
|
|
7557
|
+
createdAt: row.created_at,
|
|
7558
|
+
updatedAt: row.updated_at
|
|
7559
|
+
};
|
|
7560
|
+
}
|
|
7561
|
+
function redactPhoneTransportConfig(config) {
|
|
7562
|
+
return {
|
|
7563
|
+
...config,
|
|
7564
|
+
password: config.password ? "***" : "",
|
|
7565
|
+
webhookSecret: config.webhookSecret ? "***" : ""
|
|
7566
|
+
};
|
|
7567
|
+
}
|
|
7568
|
+
var PhoneManager = class {
|
|
7569
|
+
constructor(db2, encryptionKey) {
|
|
7570
|
+
this.db = db2;
|
|
7571
|
+
this.encryptionKey = encryptionKey;
|
|
7572
|
+
this.ensureTables();
|
|
7573
|
+
}
|
|
7574
|
+
initialized = false;
|
|
7575
|
+
/** Per-agent outbound-call timestamps (ms) for the in-memory rate limiter. */
|
|
7576
|
+
callTimestamps = /* @__PURE__ */ new Map();
|
|
7577
|
+
/**
|
|
7578
|
+
* Abuse / cost gate for /calls/start (#43-H1). Each non-dry-run call is
|
|
7579
|
+
* a real billed outbound call, so before dialing we enforce:
|
|
7580
|
+
* - a hard cap on concurrently-active (non-terminal) missions, and
|
|
7581
|
+
* - a per-agent token-bucket rate limit (per-minute + per-hour).
|
|
7582
|
+
* Throws {@link PhoneRateLimitError} (-> HTTP 429) when a limit is hit.
|
|
7583
|
+
* Call only on the real path — dry runs place no call and are exempt.
|
|
7584
|
+
*/
|
|
7585
|
+
enforceCallLimits(agentId, nowMs) {
|
|
7586
|
+
const activeRow = this.db.prepare(
|
|
7587
|
+
`SELECT COUNT(*) AS cnt FROM phone_missions
|
|
7588
|
+
WHERE agent_id = ? AND status NOT IN ('completed', 'failed', 'cancelled')`
|
|
7589
|
+
).get(agentId);
|
|
7590
|
+
if ((activeRow?.cnt ?? 0) >= PHONE_MAX_CONCURRENT_MISSIONS) {
|
|
7591
|
+
throw new PhoneRateLimitError(
|
|
7592
|
+
`Too many active phone missions (max ${PHONE_MAX_CONCURRENT_MISSIONS}). Wait for an active call to end before starting another.`
|
|
7593
|
+
);
|
|
7594
|
+
}
|
|
7595
|
+
const recent = (this.callTimestamps.get(agentId) ?? []).filter((ts) => nowMs - ts < 36e5);
|
|
7596
|
+
const lastMinute = recent.filter((ts) => nowMs - ts < 6e4).length;
|
|
7597
|
+
if (lastMinute >= PHONE_RATE_LIMIT_PER_MINUTE) {
|
|
7598
|
+
throw new PhoneRateLimitError(
|
|
7599
|
+
`Phone call rate limit reached (max ${PHONE_RATE_LIMIT_PER_MINUTE}/minute). Try again shortly.`
|
|
7600
|
+
);
|
|
7601
|
+
}
|
|
7602
|
+
if (recent.length >= PHONE_RATE_LIMIT_PER_HOUR) {
|
|
7603
|
+
throw new PhoneRateLimitError(
|
|
7604
|
+
`Phone call rate limit reached (max ${PHONE_RATE_LIMIT_PER_HOUR}/hour). Try again later.`
|
|
7605
|
+
);
|
|
7606
|
+
}
|
|
7607
|
+
recent.push(nowMs);
|
|
7608
|
+
this.callTimestamps.set(agentId, recent);
|
|
7609
|
+
}
|
|
7610
|
+
encryptConfig(config) {
|
|
7611
|
+
if (!this.encryptionKey) return config;
|
|
7612
|
+
const out = { ...config };
|
|
7613
|
+
for (const field of PHONE_SECRET_FIELDS) {
|
|
7614
|
+
const value = out[field];
|
|
7615
|
+
if (typeof value === "string" && value && !isEncryptedSecret(value)) {
|
|
7616
|
+
out[field] = encryptSecret(value, this.encryptionKey);
|
|
7617
|
+
}
|
|
7618
|
+
}
|
|
7619
|
+
return out;
|
|
7620
|
+
}
|
|
7621
|
+
decryptConfig(config) {
|
|
7622
|
+
if (!this.encryptionKey) return config;
|
|
7623
|
+
const out = { ...config };
|
|
7624
|
+
for (const field of PHONE_SECRET_FIELDS) {
|
|
7625
|
+
const value = out[field];
|
|
7626
|
+
if (typeof value === "string" && isEncryptedSecret(value)) {
|
|
7627
|
+
try {
|
|
7628
|
+
out[field] = decryptSecret(value, this.encryptionKey);
|
|
7629
|
+
} catch {
|
|
7630
|
+
}
|
|
7631
|
+
}
|
|
7632
|
+
}
|
|
7633
|
+
return out;
|
|
7634
|
+
}
|
|
7635
|
+
ensureTables() {
|
|
7636
|
+
if (this.initialized) return;
|
|
7637
|
+
this.db.exec(`
|
|
7638
|
+
CREATE TABLE IF NOT EXISTS phone_missions (
|
|
7639
|
+
id TEXT PRIMARY KEY,
|
|
7640
|
+
agent_id TEXT NOT NULL,
|
|
7641
|
+
status TEXT NOT NULL,
|
|
7642
|
+
from_phone TEXT NOT NULL,
|
|
7643
|
+
to_phone TEXT NOT NULL,
|
|
7644
|
+
task TEXT NOT NULL,
|
|
7645
|
+
policy_json TEXT NOT NULL,
|
|
7646
|
+
transport_json TEXT NOT NULL,
|
|
7647
|
+
provider TEXT NOT NULL,
|
|
7648
|
+
provider_call_id TEXT,
|
|
7649
|
+
transcript_json TEXT NOT NULL DEFAULT '[]',
|
|
7650
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
7651
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
7652
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
7653
|
+
)
|
|
7654
|
+
`);
|
|
7655
|
+
try {
|
|
7656
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_phone_missions_agent ON phone_missions(agent_id)");
|
|
7657
|
+
} catch {
|
|
7658
|
+
}
|
|
7659
|
+
try {
|
|
7660
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_phone_missions_status ON phone_missions(status)");
|
|
7661
|
+
} catch {
|
|
7662
|
+
}
|
|
7663
|
+
this.initialized = true;
|
|
7664
|
+
}
|
|
7665
|
+
getPhoneTransportConfig(agentId) {
|
|
7666
|
+
const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
|
|
7667
|
+
if (!row) return null;
|
|
7668
|
+
const meta = parseJson(row.metadata, {});
|
|
7669
|
+
const config = meta.phoneTransport;
|
|
7670
|
+
if (!config || typeof config !== "object") return null;
|
|
7671
|
+
return this.decryptConfig(config);
|
|
7672
|
+
}
|
|
7673
|
+
savePhoneTransportConfig(agentId, config) {
|
|
7674
|
+
const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
|
|
7675
|
+
if (!row) throw new Error(`Agent ${agentId} not found`);
|
|
7676
|
+
const transportCheck = validatePhoneTransportProfile(config);
|
|
7677
|
+
if (!transportCheck.ok) {
|
|
7678
|
+
throw new Error(`Invalid phone transport config: ${transportCheck.issues.map((item) => `${item.field}: ${item.message}`).join("; ")}`);
|
|
7679
|
+
}
|
|
7680
|
+
const meta = parseJson(row.metadata, {});
|
|
7681
|
+
meta.phoneTransport = this.encryptConfig({
|
|
7682
|
+
...config,
|
|
7683
|
+
phoneNumber: transportCheck.transport.phoneNumber,
|
|
7684
|
+
capabilities: transportCheck.transport.capabilities,
|
|
7685
|
+
supportedRegions: transportCheck.transport.supportedRegions
|
|
7686
|
+
});
|
|
7687
|
+
this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
|
|
7688
|
+
return config;
|
|
7689
|
+
}
|
|
7690
|
+
getMission(missionId, agentId) {
|
|
7691
|
+
const row = agentId ? this.db.prepare("SELECT * FROM phone_missions WHERE id = ? AND agent_id = ?").get(missionId, agentId) : this.db.prepare("SELECT * FROM phone_missions WHERE id = ?").get(missionId);
|
|
7692
|
+
return row ? rowToMission(row) : null;
|
|
7693
|
+
}
|
|
7694
|
+
listMissions(agentId, opts = {}) {
|
|
7695
|
+
const limit = Math.min(Math.max(opts.limit ?? 20, 1), 100);
|
|
7696
|
+
const offset = Math.max(opts.offset ?? 0, 0);
|
|
7697
|
+
const params = [agentId];
|
|
7698
|
+
let sql = "SELECT * FROM phone_missions WHERE agent_id = ?";
|
|
7699
|
+
if (opts.status && PHONE_MISSION_STATES.includes(opts.status)) {
|
|
7700
|
+
sql += " AND status = ?";
|
|
7701
|
+
params.push(opts.status);
|
|
7702
|
+
}
|
|
7703
|
+
sql += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
|
|
7704
|
+
params.push(limit, offset);
|
|
7705
|
+
return this.db.prepare(sql).all(...params).map(rowToMission);
|
|
7706
|
+
}
|
|
7707
|
+
async startMission(agentId, input, options = {}) {
|
|
7708
|
+
const config = this.getPhoneTransportConfig(agentId);
|
|
7709
|
+
if (!config) {
|
|
7710
|
+
throw new Error("Phone transport is not configured. Use phone_transport_setup first.");
|
|
7711
|
+
}
|
|
7712
|
+
if (config.provider !== "46elks") {
|
|
7713
|
+
throw new Error(`Phone provider ${config.provider} does not support call_control yet`);
|
|
7714
|
+
}
|
|
7715
|
+
const validation = validatePhoneMissionStart(input, config);
|
|
7716
|
+
if (!validation.ok) {
|
|
7717
|
+
throw new Error(`Invalid phone mission: ${validation.issues.map((item) => `${item.code} (${item.field})`).join(", ")}`);
|
|
7718
|
+
}
|
|
7719
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
7720
|
+
if (!options.dryRun) {
|
|
7721
|
+
this.enforceCallLimits(agentId, now.getTime());
|
|
7722
|
+
}
|
|
7723
|
+
const missionId = `call_${(0, import_node_crypto3.randomUUID)()}`;
|
|
7724
|
+
const transcript = [{
|
|
7725
|
+
at: now.toISOString(),
|
|
7726
|
+
source: "system",
|
|
7727
|
+
text: "Phone mission created; outbound carrier call requested."
|
|
7728
|
+
}];
|
|
7729
|
+
const metadata = {
|
|
7730
|
+
voiceRuntimeRef: validation.mission.voiceRuntimeRef,
|
|
7731
|
+
targetRegion: validation.mission.targetRegion,
|
|
7732
|
+
dryRun: !!options.dryRun,
|
|
7733
|
+
// Attempt counter (#43-H2) — wired for breach detection; there is
|
|
7734
|
+
// no automatic retry loop today, so a fresh mission is attempt 1.
|
|
7735
|
+
attempts: 1
|
|
7736
|
+
};
|
|
7737
|
+
const mission = {
|
|
7738
|
+
id: missionId,
|
|
7739
|
+
agentId,
|
|
7740
|
+
status: "dialing",
|
|
7741
|
+
from: config.phoneNumber,
|
|
7742
|
+
to: validation.mission.to,
|
|
7743
|
+
task: validation.mission.task,
|
|
7744
|
+
policy: validation.mission.policy,
|
|
7745
|
+
transport: validation.mission.transport,
|
|
7746
|
+
provider: config.provider,
|
|
7747
|
+
transcript,
|
|
7748
|
+
metadata,
|
|
7749
|
+
createdAt: now.toISOString(),
|
|
7750
|
+
updatedAt: now.toISOString()
|
|
7751
|
+
};
|
|
7752
|
+
this.insertMission(mission);
|
|
7753
|
+
const providerRequest = this.build46ElksCallRequest(config, mission);
|
|
7754
|
+
if (options.dryRun) {
|
|
7755
|
+
const updated2 = this.updateProviderCall(missionId, "dryrun-call", {
|
|
7756
|
+
dryRun: true,
|
|
7757
|
+
providerRequest: redactProviderRequest(providerRequest)
|
|
7758
|
+
});
|
|
7759
|
+
return { mission: updated2, providerRequest };
|
|
7760
|
+
}
|
|
7761
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
7762
|
+
let response;
|
|
7763
|
+
try {
|
|
7764
|
+
response = await fetchFn(providerRequest.url, {
|
|
7765
|
+
method: "POST",
|
|
7766
|
+
headers: {
|
|
7767
|
+
"Authorization": `Basic ${basicAuth2(config.username, config.password)}`,
|
|
7768
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
7769
|
+
},
|
|
7770
|
+
body: new URLSearchParams(providerRequest.body),
|
|
7771
|
+
signal: AbortSignal.timeout(15e3)
|
|
7772
|
+
});
|
|
7773
|
+
} catch (err) {
|
|
7774
|
+
const message = err?.message ?? String(err);
|
|
7775
|
+
this.updateMissionStatus(missionId, "failed", {
|
|
7776
|
+
providerError: message
|
|
7777
|
+
}, [{
|
|
7778
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7779
|
+
source: "provider",
|
|
7780
|
+
text: "46elks call start failed \u2014 the provider request threw before any response.",
|
|
7781
|
+
metadata: { error: message }
|
|
7782
|
+
}]);
|
|
7783
|
+
throw err;
|
|
7784
|
+
}
|
|
7785
|
+
const text = await response.text();
|
|
7786
|
+
let raw = text;
|
|
7787
|
+
try {
|
|
7788
|
+
raw = JSON.parse(text);
|
|
7789
|
+
} catch {
|
|
7790
|
+
}
|
|
7791
|
+
if (!response.ok) {
|
|
7792
|
+
const failed = this.updateMissionStatus(missionId, "failed", {
|
|
7793
|
+
providerStatus: response.status,
|
|
7794
|
+
providerResponse: raw
|
|
7795
|
+
}, [{
|
|
7796
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7797
|
+
source: "provider",
|
|
7798
|
+
text: `46elks call start failed with HTTP ${response.status}.`,
|
|
7799
|
+
metadata: { providerResponse: raw }
|
|
7800
|
+
}]);
|
|
7801
|
+
throw new Error(`46elks call start failed (${response.status}) for mission ${failed.id}`);
|
|
7802
|
+
}
|
|
7803
|
+
const providerCallId = asRecord2(raw).id ? String(asRecord2(raw).id) : void 0;
|
|
7804
|
+
const updated = this.updateProviderCall(missionId, providerCallId, { providerResponse: raw });
|
|
7805
|
+
return { mission: updated, providerRequest, providerResponse: raw };
|
|
7806
|
+
}
|
|
7807
|
+
/**
|
|
7808
|
+
* Verify a webhook request and return the mission, or throw a uniform
|
|
7809
|
+
* {@link PhoneWebhookAuthError} for ANY failure (unknown mission, no
|
|
7810
|
+
* token, wrong token). Uniform on purpose — no 404-vs-403 oracle.
|
|
7811
|
+
*/
|
|
7812
|
+
authenticateWebhook(missionId, providedToken) {
|
|
7813
|
+
const mission = missionId ? this.getMission(missionId) : null;
|
|
7814
|
+
const config = mission ? this.getPhoneTransportConfig(mission.agentId) : null;
|
|
7815
|
+
if (!mission || !config || !providedToken || !secretMatches(providedToken, webhookToken(config.webhookSecret, mission.id))) {
|
|
7816
|
+
throw new PhoneWebhookAuthError();
|
|
7817
|
+
}
|
|
7818
|
+
return mission;
|
|
7819
|
+
}
|
|
7820
|
+
handleVoiceStartWebhook(missionId, providedToken, payload = {}) {
|
|
7821
|
+
const mission = this.authenticateWebhook(missionId, providedToken);
|
|
7822
|
+
if (TERMINAL_MISSION_STATES.includes(mission.status)) {
|
|
7823
|
+
return { mission, action: this.buildVoiceStartAction() };
|
|
7824
|
+
}
|
|
7825
|
+
const eventKey = phoneWebhookEventKey("voice_start", payload);
|
|
7826
|
+
if (hasProcessedWebhookEvent(mission, eventKey)) {
|
|
7827
|
+
return {
|
|
7828
|
+
mission,
|
|
7829
|
+
action: this.buildVoiceStartAction()
|
|
7830
|
+
};
|
|
7831
|
+
}
|
|
7832
|
+
const updated = this.updateMissionStatus(mission.id, "connected", {
|
|
7833
|
+
lastVoiceStartPayload: payload,
|
|
7834
|
+
phoneWebhookEvents: appendProcessedWebhookEvent(mission, eventKey)
|
|
7835
|
+
}, [{
|
|
7836
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7837
|
+
source: "provider",
|
|
7838
|
+
text: "46elks voice_start webhook received. Realtime voice runtime is not connected in this slice.",
|
|
7839
|
+
metadata: { payload }
|
|
7840
|
+
}]);
|
|
7841
|
+
return {
|
|
7842
|
+
mission: updated,
|
|
7843
|
+
action: this.buildVoiceStartAction()
|
|
7844
|
+
};
|
|
7845
|
+
}
|
|
7846
|
+
handleHangupWebhook(missionId, providedToken, payload = {}) {
|
|
7847
|
+
const mission = this.authenticateWebhook(missionId, providedToken);
|
|
7848
|
+
const eventKey = phoneWebhookEventKey("hangup", payload);
|
|
7849
|
+
if (hasProcessedWebhookEvent(mission, eventKey)) {
|
|
7850
|
+
return mission;
|
|
7851
|
+
}
|
|
7852
|
+
const costPatch = this.buildCostMetadataPatch(mission, payload);
|
|
7853
|
+
const nextStatus = TERMINAL_MISSION_STATES.includes(mission.status) ? mission.status : "failed";
|
|
7854
|
+
const transcript = [{
|
|
7855
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7856
|
+
source: "provider",
|
|
7857
|
+
text: nextStatus === "failed" ? "46elks hangup webhook received before a conversation runtime completed the mission." : "46elks hangup webhook received.",
|
|
7858
|
+
metadata: { payload }
|
|
7859
|
+
}];
|
|
7860
|
+
if (costPatch.costExceeded) {
|
|
7861
|
+
transcript.push({
|
|
7862
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7863
|
+
source: "system",
|
|
7864
|
+
text: `Mission cost ${costPatch.totalCost} exceeded the policy cap of ${mission.policy.maxCostPerMission}.`
|
|
7865
|
+
});
|
|
7866
|
+
}
|
|
7867
|
+
return this.updateMissionStatus(mission.id, nextStatus, {
|
|
7868
|
+
lastHangupPayload: payload,
|
|
7869
|
+
hangupReason: nextStatus === "failed" ? "call-ended-before-conversation-runtime" : void 0,
|
|
7870
|
+
phoneWebhookEvents: appendProcessedWebhookEvent(mission, eventKey),
|
|
7871
|
+
...costPatch
|
|
7872
|
+
}, transcript);
|
|
7873
|
+
}
|
|
7874
|
+
/**
|
|
7875
|
+
* Read the call cost off a 46elks hangup payload, add it to the
|
|
7876
|
+
* mission's running total, and flag a policy-cap breach (#43-H2).
|
|
7877
|
+
* Cost is only knowable post-call from the provider — the preventive
|
|
7878
|
+
* cost controls are the duration ceiling, rate limit, and concurrency
|
|
7879
|
+
* cap; this is the after-the-fact accounting + alerting.
|
|
7880
|
+
*/
|
|
7881
|
+
buildCostMetadataPatch(mission, payload) {
|
|
7882
|
+
const rawCost = payload.cost;
|
|
7883
|
+
const callCost = typeof rawCost === "number" && Number.isFinite(rawCost) && rawCost >= 0 ? rawCost : Number.parseFloat(asString3(rawCost)) || 0;
|
|
7884
|
+
const priorCost = typeof mission.metadata.totalCost === "number" ? mission.metadata.totalCost : 0;
|
|
7885
|
+
const totalCost = Math.round((priorCost + callCost) * 1e6) / 1e6;
|
|
7886
|
+
const cap = mission.policy?.maxCostPerMission;
|
|
7887
|
+
const costExceeded = typeof cap === "number" && totalCost > cap;
|
|
7888
|
+
return { totalCost, costExceeded };
|
|
7889
|
+
}
|
|
7890
|
+
buildVoiceStartAction() {
|
|
7891
|
+
return {
|
|
7892
|
+
play: "AgenticMail has received this call mission. The live voice runtime is not connected yet; the operator will follow up."
|
|
7893
|
+
};
|
|
7894
|
+
}
|
|
7895
|
+
cancelMission(agentId, missionId) {
|
|
7896
|
+
const mission = this.getMission(missionId, agentId);
|
|
7897
|
+
if (!mission) throw new Error("Phone mission not found");
|
|
7898
|
+
return this.updateMissionStatus(mission.id, "cancelled", {}, [{
|
|
7899
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7900
|
+
source: "operator",
|
|
7901
|
+
text: "Phone mission cancelled."
|
|
7902
|
+
}]);
|
|
7903
|
+
}
|
|
7904
|
+
build46ElksCallRequest(config, mission) {
|
|
7905
|
+
const timeout = Math.min(Math.max(mission.policy.maxCallDurationSeconds, 1), PHONE_SERVER_MAX_CALL_DURATION_SECONDS);
|
|
7906
|
+
return {
|
|
7907
|
+
url: `${defaultApiUrl2(config)}/calls`,
|
|
7908
|
+
body: {
|
|
7909
|
+
from: config.phoneNumber,
|
|
7910
|
+
to: mission.to,
|
|
7911
|
+
voice_start: buildWebhookUrl(config, "/calls/webhook/46elks/voice-start", mission.id),
|
|
7912
|
+
whenhangup: buildWebhookUrl(config, "/calls/webhook/46elks/hangup", mission.id),
|
|
7913
|
+
timeout: String(timeout)
|
|
7914
|
+
}
|
|
7915
|
+
};
|
|
7916
|
+
}
|
|
7917
|
+
insertMission(mission) {
|
|
7918
|
+
this.db.prepare(`
|
|
7919
|
+
INSERT INTO phone_missions (
|
|
7920
|
+
id, agent_id, status, from_phone, to_phone, task,
|
|
7921
|
+
policy_json, transport_json, provider, provider_call_id,
|
|
7922
|
+
transcript_json, metadata_json, created_at, updated_at
|
|
7923
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7924
|
+
`).run(
|
|
7925
|
+
mission.id,
|
|
7926
|
+
mission.agentId,
|
|
7927
|
+
mission.status,
|
|
7928
|
+
mission.from,
|
|
7929
|
+
mission.to,
|
|
7930
|
+
mission.task,
|
|
7931
|
+
JSON.stringify(mission.policy),
|
|
7932
|
+
JSON.stringify(mission.transport),
|
|
7933
|
+
mission.provider,
|
|
7934
|
+
mission.providerCallId ?? null,
|
|
7935
|
+
JSON.stringify(mission.transcript),
|
|
7936
|
+
JSON.stringify(mission.metadata),
|
|
7937
|
+
mission.createdAt,
|
|
7938
|
+
mission.updatedAt
|
|
7939
|
+
);
|
|
7940
|
+
}
|
|
7941
|
+
updateProviderCall(missionId, providerCallId, metadata) {
|
|
7942
|
+
const mission = this.getMission(missionId);
|
|
7943
|
+
if (!mission) throw new Error("Phone mission not found");
|
|
7944
|
+
const nextMetadata = { ...mission.metadata, ...metadata };
|
|
7945
|
+
this.db.prepare(`
|
|
7946
|
+
UPDATE phone_missions
|
|
7947
|
+
SET provider_call_id = ?, metadata_json = ?, updated_at = ?
|
|
7948
|
+
WHERE id = ?
|
|
7949
|
+
`).run(providerCallId ?? null, JSON.stringify(nextMetadata), (/* @__PURE__ */ new Date()).toISOString(), missionId);
|
|
7950
|
+
return this.getMission(missionId);
|
|
7951
|
+
}
|
|
7952
|
+
updateMissionStatus(missionId, status, metadata, transcriptEntries = []) {
|
|
7953
|
+
const mission = this.getMission(missionId);
|
|
7954
|
+
if (!mission) throw new Error("Phone mission not found");
|
|
7955
|
+
const nextTranscript = [...mission.transcript, ...transcriptEntries];
|
|
7956
|
+
const nextMetadata = Object.fromEntries(
|
|
7957
|
+
Object.entries({ ...mission.metadata, ...metadata }).filter(([, value]) => value !== void 0)
|
|
7958
|
+
);
|
|
7959
|
+
this.db.prepare(`
|
|
7960
|
+
UPDATE phone_missions
|
|
7961
|
+
SET status = ?, transcript_json = ?, metadata_json = ?, updated_at = ?
|
|
7962
|
+
WHERE id = ?
|
|
7963
|
+
`).run(status, JSON.stringify(nextTranscript), JSON.stringify(nextMetadata), (/* @__PURE__ */ new Date()).toISOString(), missionId);
|
|
7964
|
+
return this.getMission(missionId);
|
|
7965
|
+
}
|
|
7966
|
+
};
|
|
7967
|
+
function buildPhoneTransportConfig(input) {
|
|
7968
|
+
const provider = asString3(input.provider) || "46elks";
|
|
7969
|
+
if (provider !== "46elks") throw new Error('provider must be "46elks"');
|
|
7970
|
+
const phoneNumber = normalizePhoneNumber(asString3(input.phoneNumber));
|
|
7971
|
+
if (!phoneNumber) throw new Error("phoneNumber must be a valid E.164 phone number");
|
|
7972
|
+
const username = asString3(input.username);
|
|
7973
|
+
const password = asString3(input.password);
|
|
7974
|
+
const webhookBaseUrl = asString3(input.webhookBaseUrl);
|
|
7975
|
+
const webhookSecret = asString3(input.webhookSecret);
|
|
7976
|
+
if (!username || !password) throw new Error('username and password are required for provider "46elks"');
|
|
7977
|
+
if (!webhookBaseUrl) throw new Error("webhookBaseUrl is required");
|
|
7978
|
+
if (!webhookSecret) throw new Error("webhookSecret is required");
|
|
7979
|
+
if (webhookSecret.length < PHONE_MIN_WEBHOOK_SECRET_LENGTH) {
|
|
7980
|
+
throw new Error(`webhookSecret must be at least ${PHONE_MIN_WEBHOOK_SECRET_LENGTH} characters`);
|
|
7981
|
+
}
|
|
7982
|
+
const parsedWebhookBaseUrl = new URL(webhookBaseUrl);
|
|
7983
|
+
if (parsedWebhookBaseUrl.protocol !== "https:" && parsedWebhookBaseUrl.hostname !== "127.0.0.1" && parsedWebhookBaseUrl.hostname !== "localhost") {
|
|
7984
|
+
throw new Error("webhookBaseUrl must use https:// unless it points at localhost");
|
|
7985
|
+
}
|
|
7986
|
+
const apiUrl = asString3(input.apiUrl);
|
|
7987
|
+
if (apiUrl) {
|
|
7988
|
+
const parsedApiUrl = new URL(apiUrl);
|
|
7989
|
+
if (parsedApiUrl.protocol !== "https:") {
|
|
7990
|
+
throw new Error("apiUrl must use https:// \u2014 credentials are sent on every request");
|
|
7991
|
+
}
|
|
7992
|
+
}
|
|
7993
|
+
const capabilities = Array.isArray(input.capabilities) ? input.capabilities.filter((item) => typeof item === "string" && ["sms", "call_control", "realtime_media", "recording_supported"].includes(item)) : ["call_control"];
|
|
7994
|
+
const supportedRegions = Array.isArray(input.supportedRegions) ? input.supportedRegions.filter((item) => typeof item === "string" && ["AT", "DE", "EU", "WORLD"].includes(item)) : ["EU"];
|
|
7995
|
+
const config = {
|
|
7996
|
+
provider,
|
|
7997
|
+
phoneNumber,
|
|
7998
|
+
username,
|
|
7999
|
+
password,
|
|
8000
|
+
webhookBaseUrl,
|
|
8001
|
+
webhookSecret,
|
|
8002
|
+
apiUrl: apiUrl || void 0,
|
|
8003
|
+
capabilities: Array.from(/* @__PURE__ */ new Set(["call_control", ...capabilities])),
|
|
8004
|
+
supportedRegions: supportedRegions.length ? Array.from(new Set(supportedRegions)) : ["EU"],
|
|
8005
|
+
configuredAt: input.configuredAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
8006
|
+
};
|
|
8007
|
+
const validation = validatePhoneTransportProfile(config);
|
|
8008
|
+
if (!validation.ok) {
|
|
8009
|
+
throw new Error(`Invalid phone transport config: ${validation.issues.map((item) => `${item.field}: ${item.message}`).join("; ")}`);
|
|
8010
|
+
}
|
|
8011
|
+
return config;
|
|
8012
|
+
}
|
|
8013
|
+
|
|
7013
8014
|
// src/telemetry.ts
|
|
7014
8015
|
var import_crypto = require("crypto");
|
|
7015
8016
|
var import_fs = require("fs");
|
|
@@ -7475,7 +8476,7 @@ function buildApiUrl(baseOrigin, pathAndQuery) {
|
|
|
7475
8476
|
}
|
|
7476
8477
|
|
|
7477
8478
|
// src/setup/index.ts
|
|
7478
|
-
var
|
|
8479
|
+
var import_node_crypto4 = require("crypto");
|
|
7479
8480
|
var import_node_fs9 = require("fs");
|
|
7480
8481
|
var import_node_path11 = require("path");
|
|
7481
8482
|
var import_node_os9 = require("os");
|
|
@@ -8686,8 +9687,8 @@ var SetupManager = class {
|
|
|
8686
9687
|
if (!(0, import_node_fs9.existsSync)(dataDir)) {
|
|
8687
9688
|
(0, import_node_fs9.mkdirSync)(dataDir, { recursive: true });
|
|
8688
9689
|
}
|
|
8689
|
-
const masterKey = `mk_${(0,
|
|
8690
|
-
const stalwartPassword = (0,
|
|
9690
|
+
const masterKey = `mk_${(0, import_node_crypto4.randomBytes)(24).toString("hex")}`;
|
|
9691
|
+
const stalwartPassword = (0, import_node_crypto4.randomBytes)(16).toString("hex");
|
|
8691
9692
|
const config = {
|
|
8692
9693
|
masterKey,
|
|
8693
9694
|
stalwart: {
|
|
@@ -8825,7 +9826,7 @@ secret = "${password}"
|
|
|
8825
9826
|
};
|
|
8826
9827
|
|
|
8827
9828
|
// src/threading/thread-id.ts
|
|
8828
|
-
var
|
|
9829
|
+
var import_node_crypto5 = require("crypto");
|
|
8829
9830
|
function stripReplyPrefixes(subject) {
|
|
8830
9831
|
let s = subject.length > 1e3 ? subject.slice(0, 1e3) : subject;
|
|
8831
9832
|
for (; ; ) {
|
|
@@ -8854,7 +9855,7 @@ function normalizeAddress(addr) {
|
|
|
8854
9855
|
}
|
|
8855
9856
|
function threadIdFor(input) {
|
|
8856
9857
|
const subject = normalizeSubject(input.subject);
|
|
8857
|
-
return (0,
|
|
9858
|
+
return (0, import_node_crypto5.createHash)("sha256").update(subject).digest("base64url").slice(0, 16);
|
|
8858
9859
|
}
|
|
8859
9860
|
|
|
8860
9861
|
// src/threading/thread-cache.ts
|
|
@@ -9115,12 +10116,26 @@ function parse(raw) {
|
|
|
9115
10116
|
DependencyInstaller,
|
|
9116
10117
|
DomainManager,
|
|
9117
10118
|
DomainPurchaser,
|
|
10119
|
+
ELKS_REALTIME_AUDIO_FORMATS,
|
|
9118
10120
|
EmailSearchIndex,
|
|
9119
10121
|
GatewayManager,
|
|
9120
10122
|
InboxWatcher,
|
|
9121
10123
|
MailReceiver,
|
|
9122
10124
|
MailSender,
|
|
10125
|
+
PHONE_MAX_CONCURRENT_MISSIONS,
|
|
10126
|
+
PHONE_MIN_WEBHOOK_SECRET_LENGTH,
|
|
10127
|
+
PHONE_MISSION_STATES,
|
|
10128
|
+
PHONE_RATE_LIMIT_PER_HOUR,
|
|
10129
|
+
PHONE_RATE_LIMIT_PER_MINUTE,
|
|
10130
|
+
PHONE_REGION_SCOPES,
|
|
10131
|
+
PHONE_SERVER_MAX_ATTEMPTS,
|
|
10132
|
+
PHONE_SERVER_MAX_CALL_DURATION_SECONDS,
|
|
10133
|
+
PHONE_SERVER_MAX_COST_PER_MISSION,
|
|
10134
|
+
PHONE_TASK_MAX_LENGTH,
|
|
9123
10135
|
PathTraversalError,
|
|
10136
|
+
PhoneManager,
|
|
10137
|
+
PhoneRateLimitError,
|
|
10138
|
+
PhoneWebhookAuthError,
|
|
9124
10139
|
REDACTED,
|
|
9125
10140
|
RELAY_PRESETS,
|
|
9126
10141
|
RelayBridge,
|
|
@@ -9131,6 +10146,7 @@ function parse(raw) {
|
|
|
9131
10146
|
SmsManager,
|
|
9132
10147
|
SmsPoller,
|
|
9133
10148
|
StalwartAdmin,
|
|
10149
|
+
TELEPHONY_TRANSPORT_CAPABILITIES,
|
|
9134
10150
|
ThreadCache,
|
|
9135
10151
|
TunnelManager,
|
|
9136
10152
|
UnsafeApiUrlError,
|
|
@@ -9139,8 +10155,16 @@ function parse(raw) {
|
|
|
9139
10155
|
bridgeWakeErrorMessage,
|
|
9140
10156
|
bridgeWakeLastSeenAgeMs,
|
|
9141
10157
|
buildApiUrl,
|
|
10158
|
+
buildElksAudioMessage,
|
|
10159
|
+
buildElksByeMessage,
|
|
10160
|
+
buildElksHandshakeMessages,
|
|
10161
|
+
buildElksInterruptMessage,
|
|
10162
|
+
buildElksListeningMessage,
|
|
10163
|
+
buildElksSendingMessage,
|
|
9142
10164
|
buildInboundSecurityAdvisory,
|
|
10165
|
+
buildPhoneTransportConfig,
|
|
9143
10166
|
classifyEmailRoute,
|
|
10167
|
+
classifyPhoneNumberRisk,
|
|
9144
10168
|
classifyResumeError,
|
|
9145
10169
|
closeDatabase,
|
|
9146
10170
|
composeBridgeWakePrompt,
|
|
@@ -9155,7 +10179,10 @@ function parse(raw) {
|
|
|
9155
10179
|
getOperatorEmail,
|
|
9156
10180
|
getSmsProvider,
|
|
9157
10181
|
hostSessionStoragePath,
|
|
10182
|
+
inferPhoneRegion,
|
|
9158
10183
|
isInternalEmail,
|
|
10184
|
+
isLoopbackMailHost,
|
|
10185
|
+
isPhoneRegionAllowed,
|
|
9159
10186
|
isSessionFresh,
|
|
9160
10187
|
isValidPhoneNumber,
|
|
9161
10188
|
loadHostSession,
|
|
@@ -9164,14 +10191,17 @@ function parse(raw) {
|
|
|
9164
10191
|
normalizePhoneNumber,
|
|
9165
10192
|
normalizeSubject,
|
|
9166
10193
|
operatorPrefsStoragePath,
|
|
10194
|
+
parseElksRealtimeMessage,
|
|
9167
10195
|
parseEmail,
|
|
9168
10196
|
parseGoogleVoiceSms,
|
|
9169
10197
|
planBridgeWake,
|
|
9170
10198
|
recordToolCall,
|
|
9171
10199
|
redactObject,
|
|
10200
|
+
redactPhoneTransportConfig,
|
|
9172
10201
|
redactSecret,
|
|
9173
10202
|
redactSmsConfig,
|
|
9174
10203
|
resolveConfig,
|
|
10204
|
+
resolveTlsRejectUnauthorized,
|
|
9175
10205
|
safeJoin,
|
|
9176
10206
|
sanitizeEmail,
|
|
9177
10207
|
saveConfig,
|
|
@@ -9184,5 +10214,8 @@ function parse(raw) {
|
|
|
9184
10214
|
startRelayBridge,
|
|
9185
10215
|
threadIdFor,
|
|
9186
10216
|
tryJoin,
|
|
9187
|
-
validateApiUrl
|
|
10217
|
+
validateApiUrl,
|
|
10218
|
+
validatePhoneMissionPolicy,
|
|
10219
|
+
validatePhoneMissionStart,
|
|
10220
|
+
validatePhoneTransportProfile
|
|
9188
10221
|
});
|