@sinch/functions-runtime 0.3.8 → 0.4.0
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/bin/sinch-runtime.js +346 -48
- package/dist/bin/sinch-runtime.js.map +1 -1
- package/dist/index.d.ts +1101 -65
- package/dist/index.js +1268 -152
- package/dist/index.js.map +1 -1
- package/package.json +1 -4
package/dist/index.js
CHANGED
|
@@ -168,6 +168,21 @@ var IceSvamlBuilder = class extends BaseSvamlBuilder {
|
|
|
168
168
|
*
|
|
169
169
|
* @param number - E.164 formatted phone number (e.g., '+15551234567')
|
|
170
170
|
* @param options - Connection options
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* // Simple connect
|
|
175
|
+
* new IceSvamlBuilder().connectPstn('+15551234567').build();
|
|
176
|
+
*
|
|
177
|
+
* // With options: caller ID, timeout, and AMD
|
|
178
|
+
* new IceSvamlBuilder()
|
|
179
|
+
* .connectPstn('+15551234567', {
|
|
180
|
+
* cli: '+15559876543',
|
|
181
|
+
* timeout: 30,
|
|
182
|
+
* amd: { enabled: true },
|
|
183
|
+
* })
|
|
184
|
+
* .build();
|
|
185
|
+
* ```
|
|
171
186
|
*/
|
|
172
187
|
connectPstn(number, options) {
|
|
173
188
|
this.action = {
|
|
@@ -224,6 +239,22 @@ var IceSvamlBuilder = class extends BaseSvamlBuilder {
|
|
|
224
239
|
*
|
|
225
240
|
* @param menus - Array of menu definitions or MenuStructure from createMenu().build()
|
|
226
241
|
* @param options - Menu options
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* // Using MenuTemplates
|
|
246
|
+
* new IceSvamlBuilder()
|
|
247
|
+
* .runMenu(MenuTemplates.business('Acme Corp'))
|
|
248
|
+
* .build();
|
|
249
|
+
*
|
|
250
|
+
* // Using createMenu builder
|
|
251
|
+
* const menu = createMenu()
|
|
252
|
+
* .prompt('Press 1 for sales, 2 for support.')
|
|
253
|
+
* .option('1', 'return(sales)')
|
|
254
|
+
* .option('2', 'return(support)')
|
|
255
|
+
* .build();
|
|
256
|
+
* new IceSvamlBuilder().runMenu(menu).build();
|
|
257
|
+
* ```
|
|
227
258
|
*/
|
|
228
259
|
runMenu(menus, options) {
|
|
229
260
|
const menuArray = Array.isArray(menus) ? menus : menus.menus;
|
|
@@ -241,6 +272,14 @@ var IceSvamlBuilder = class extends BaseSvamlBuilder {
|
|
|
241
272
|
*
|
|
242
273
|
* @param holdPrompt - Prompt to play while on hold
|
|
243
274
|
* @param options - Park options
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```typescript
|
|
278
|
+
* // Park with hold music prompt
|
|
279
|
+
* new IceSvamlBuilder()
|
|
280
|
+
* .park('Please hold while we connect you.', { maxDuration: 120 })
|
|
281
|
+
* .build();
|
|
282
|
+
* ```
|
|
244
283
|
*/
|
|
245
284
|
park(holdPrompt, options) {
|
|
246
285
|
this.action = {
|
|
@@ -982,11 +1021,11 @@ function isVoiceCallback(functionName) {
|
|
|
982
1021
|
function isNotificationEvent(functionName) {
|
|
983
1022
|
return NOTIFICATION_EVENTS.includes(functionName);
|
|
984
1023
|
}
|
|
985
|
-
function extractFunctionName(
|
|
1024
|
+
function extractFunctionName(path6, body) {
|
|
986
1025
|
if (body?.event && isVoiceCallback(body.event)) {
|
|
987
1026
|
return body.event;
|
|
988
1027
|
}
|
|
989
|
-
const pathname =
|
|
1028
|
+
const pathname = path6.split("?")[0];
|
|
990
1029
|
const segments = pathname.split("/").filter((s) => s && s !== "*");
|
|
991
1030
|
if (segments.length === 1 && isVoiceCallback(segments[0])) {
|
|
992
1031
|
return segments[0];
|
|
@@ -1003,10 +1042,10 @@ function generateRequestId() {
|
|
|
1003
1042
|
return `req_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
1004
1043
|
}
|
|
1005
1044
|
function findFunctionPath() {
|
|
1006
|
-
const
|
|
1045
|
+
const fs5 = requireCjs2("fs");
|
|
1007
1046
|
const distPath = nodePath.join(process.cwd(), "dist", "function.js");
|
|
1008
1047
|
const rootPath = nodePath.join(process.cwd(), "function.js");
|
|
1009
|
-
if (
|
|
1048
|
+
if (fs5.existsSync(distPath)) {
|
|
1010
1049
|
return distPath;
|
|
1011
1050
|
}
|
|
1012
1051
|
return rootPath;
|
|
@@ -1084,6 +1123,15 @@ var noOpCache = {
|
|
|
1084
1123
|
keys: async () => [],
|
|
1085
1124
|
getMany: async () => ({})
|
|
1086
1125
|
};
|
|
1126
|
+
var noOpStorage = {
|
|
1127
|
+
write: async () => {
|
|
1128
|
+
},
|
|
1129
|
+
read: async () => Buffer.alloc(0),
|
|
1130
|
+
list: async () => [],
|
|
1131
|
+
exists: async () => false,
|
|
1132
|
+
delete: async () => {
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1087
1135
|
function buildBaseContext(req, config = {}) {
|
|
1088
1136
|
return {
|
|
1089
1137
|
requestId: req.headers["x-request-id"] || generateRequestId(),
|
|
@@ -1096,6 +1144,8 @@ function buildBaseContext(req, config = {}) {
|
|
|
1096
1144
|
variables: config.variables
|
|
1097
1145
|
},
|
|
1098
1146
|
cache: noOpCache,
|
|
1147
|
+
storage: noOpStorage,
|
|
1148
|
+
database: "",
|
|
1099
1149
|
assets: (filename) => {
|
|
1100
1150
|
const filePath = nodePath.join(process.cwd(), "assets", filename);
|
|
1101
1151
|
return nodeFs.promises.readFile(filePath, "utf-8");
|
|
@@ -1412,145 +1462,895 @@ var ElevenLabsStateManager = class {
|
|
|
1412
1462
|
};
|
|
1413
1463
|
var ElevenLabsState = new ElevenLabsStateManager();
|
|
1414
1464
|
|
|
1415
|
-
// ../runtime-shared/dist/
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
var
|
|
1465
|
+
// ../runtime-shared/dist/ai/elevenlabs/client.js
|
|
1466
|
+
var ELEVENLABS_API_BASE = "https://api.elevenlabs.io/v1";
|
|
1467
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
1468
|
+
var ElevenLabsClient = class {
|
|
1469
|
+
apiKey;
|
|
1470
|
+
constructor(apiKey) {
|
|
1471
|
+
this.apiKey = apiKey || process.env.ELEVENLABS_API_KEY || "";
|
|
1472
|
+
if (!this.apiKey) {
|
|
1473
|
+
console.warn("[ElevenLabs] No API key provided. Set ELEVENLABS_API_KEY environment variable.");
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
get authHeaders() {
|
|
1477
|
+
return { "xi-api-key": this.apiKey, "Content-Type": "application/json" };
|
|
1478
|
+
}
|
|
1419
1479
|
/**
|
|
1420
|
-
*
|
|
1421
|
-
* @param templatePath - Path to the HTML template file
|
|
1422
|
-
* @param variables - Object containing variables to replace in template
|
|
1423
|
-
* @returns Rendered HTML string
|
|
1480
|
+
* Initiate an outbound call to a phone number using an ElevenLabs agent
|
|
1424
1481
|
*/
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
console.error("Error rendering template:", error);
|
|
1437
|
-
throw new Error(`Failed to render template: ${templatePath}`);
|
|
1482
|
+
async makeCall(options) {
|
|
1483
|
+
const phoneNumberId = ElevenLabsState.getPhoneNumberId();
|
|
1484
|
+
const request = {
|
|
1485
|
+
agent_id: options.agentId,
|
|
1486
|
+
customer_phone_number: options.toNumber,
|
|
1487
|
+
agent_phone_number_id: phoneNumberId
|
|
1488
|
+
};
|
|
1489
|
+
if (options.dynamicVariables && Object.keys(options.dynamicVariables).length > 0) {
|
|
1490
|
+
request.custom_llm_extra_body = {
|
|
1491
|
+
dynamic_variables: options.dynamicVariables
|
|
1492
|
+
};
|
|
1438
1493
|
}
|
|
1494
|
+
const response = await fetch(`${ELEVENLABS_API_BASE}/convai/conversation/create_call`, {
|
|
1495
|
+
method: "POST",
|
|
1496
|
+
headers: this.authHeaders,
|
|
1497
|
+
body: JSON.stringify(request),
|
|
1498
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1499
|
+
});
|
|
1500
|
+
if (!response.ok) {
|
|
1501
|
+
const error = await response.text();
|
|
1502
|
+
throw new Error(`ElevenLabs API error: ${response.status} - ${error}`);
|
|
1503
|
+
}
|
|
1504
|
+
return response.json();
|
|
1439
1505
|
}
|
|
1440
1506
|
/**
|
|
1441
|
-
* Get
|
|
1442
|
-
* @param templateName - Name of the template file (e.g., 'index.html')
|
|
1443
|
-
* @param baseDir - Base directory to search from (defaults to cwd)
|
|
1444
|
-
* @returns Absolute path to the template file
|
|
1507
|
+
* Get conversation details including transcript and analysis
|
|
1445
1508
|
*/
|
|
1446
|
-
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1509
|
+
async getConversationDetails(conversationId) {
|
|
1510
|
+
const response = await fetch(`${ELEVENLABS_API_BASE}/convai/conversations/${encodeURIComponent(conversationId)}`, {
|
|
1511
|
+
headers: { "xi-api-key": this.apiKey },
|
|
1512
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1513
|
+
});
|
|
1514
|
+
if (!response.ok) {
|
|
1515
|
+
const error = await response.text();
|
|
1516
|
+
throw new Error(`ElevenLabs API error: ${response.status} - ${error}`);
|
|
1517
|
+
}
|
|
1518
|
+
const data = await response.json();
|
|
1519
|
+
return this.mapConversationResponse(data);
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Auto-configure ElevenLabs with Sinch SIP trunk.
|
|
1523
|
+
*
|
|
1524
|
+
* Step 1: Get or import phone number with SIP trunk config
|
|
1525
|
+
* Step 2: Configure agent conversation-init webhook
|
|
1526
|
+
*
|
|
1527
|
+
* Called by tryAutoConfigureAsync (the orchestrator that reads env vars).
|
|
1528
|
+
*/
|
|
1529
|
+
async autoConfigureAsync(options) {
|
|
1530
|
+
console.log(`[ElevenLabs] Starting auto-configuration for agent ${options.agentId}`);
|
|
1531
|
+
const phoneResult = await this.getOrImportPhoneNumber(options);
|
|
1532
|
+
if (options.webhookBaseUrl) {
|
|
1533
|
+
await this.configureAgentWebhooks(options.agentId, options.webhookBaseUrl);
|
|
1534
|
+
}
|
|
1535
|
+
console.log(`[ElevenLabs] Auto-configuration completed: PhoneNumberId=${phoneResult.phoneNumberId}`);
|
|
1536
|
+
return {
|
|
1537
|
+
success: true,
|
|
1538
|
+
phoneNumberId: phoneResult.phoneNumberId,
|
|
1539
|
+
phoneNumber: options.phoneNumber,
|
|
1540
|
+
sipAddress: options.sipAddress
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Check if phone number already exists in ElevenLabs; create or update it.
|
|
1545
|
+
* Matches C# GetOrImportPhoneNumberAsync.
|
|
1546
|
+
*/
|
|
1547
|
+
async getOrImportPhoneNumber(config) {
|
|
1548
|
+
const listResponse = await fetch(`${ELEVENLABS_API_BASE}/convai/phone-numbers`, {
|
|
1549
|
+
headers: this.authHeaders,
|
|
1550
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1551
|
+
});
|
|
1552
|
+
if (!listResponse.ok) {
|
|
1553
|
+
throw new Error(`Failed to list phone numbers: ${listResponse.status}`);
|
|
1554
|
+
}
|
|
1555
|
+
const phoneNumbers = await listResponse.json();
|
|
1556
|
+
const sipConfig = {
|
|
1557
|
+
address: config.sipAddress,
|
|
1558
|
+
transport: "tcp",
|
|
1559
|
+
media_encryption: "disabled",
|
|
1560
|
+
credentials: { username: config.sipUsername, password: config.sipPassword }
|
|
1561
|
+
};
|
|
1562
|
+
const existing = phoneNumbers.find((p) => p.phone_number === config.phoneNumber);
|
|
1563
|
+
if (existing) {
|
|
1564
|
+
console.log(`[ElevenLabs] Phone number already exists (${existing.phone_number_id}), updating config`);
|
|
1565
|
+
await this.updatePhoneNumberConfig(existing.phone_number_id, config.agentId, sipConfig);
|
|
1566
|
+
return { phoneNumberId: existing.phone_number_id };
|
|
1567
|
+
}
|
|
1568
|
+
const label = `Sinch Functions - ${config.functionName || "Sinch Function"}`;
|
|
1569
|
+
console.log("[ElevenLabs] Creating SIP trunk phone number");
|
|
1570
|
+
const createResponse2 = await fetch(`${ELEVENLABS_API_BASE}/convai/phone-numbers`, {
|
|
1571
|
+
method: "POST",
|
|
1572
|
+
headers: this.authHeaders,
|
|
1573
|
+
body: JSON.stringify({
|
|
1574
|
+
phone_number: config.phoneNumber,
|
|
1575
|
+
label,
|
|
1576
|
+
provider: "sip_trunk",
|
|
1577
|
+
supports_inbound: true,
|
|
1578
|
+
supports_outbound: true,
|
|
1579
|
+
inbound_trunk_config: { media_encryption: "disabled" },
|
|
1580
|
+
outbound_trunk_config: sipConfig
|
|
1581
|
+
}),
|
|
1582
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1583
|
+
});
|
|
1584
|
+
if (!createResponse2.ok) {
|
|
1585
|
+
const err = await createResponse2.text();
|
|
1586
|
+
throw new Error(`Failed to create phone number: ${createResponse2.status} - ${err}`);
|
|
1587
|
+
}
|
|
1588
|
+
const created = await createResponse2.json();
|
|
1589
|
+
console.log("[ElevenLabs] Phone number created successfully");
|
|
1590
|
+
await this.updatePhoneNumberConfig(created.phone_number_id, config.agentId, sipConfig);
|
|
1591
|
+
return { phoneNumberId: created.phone_number_id };
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Update phone number with agent association and SIP trunk config.
|
|
1595
|
+
* Matches C# UpdatePhoneNumberConfigAsync.
|
|
1596
|
+
*/
|
|
1597
|
+
async updatePhoneNumberConfig(phoneNumberId, agentId, sipConfig) {
|
|
1598
|
+
const response = await fetch(`${ELEVENLABS_API_BASE}/convai/phone-numbers/${encodeURIComponent(phoneNumberId)}`, {
|
|
1599
|
+
method: "PATCH",
|
|
1600
|
+
headers: this.authHeaders,
|
|
1601
|
+
body: JSON.stringify({
|
|
1602
|
+
agent_id: agentId,
|
|
1603
|
+
outbound_trunk_config: sipConfig
|
|
1604
|
+
}),
|
|
1605
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1606
|
+
});
|
|
1607
|
+
if (!response.ok) {
|
|
1608
|
+
await response.text();
|
|
1609
|
+
console.warn(`[ElevenLabs] Failed to update phone number config: ${response.status}`);
|
|
1610
|
+
} else {
|
|
1611
|
+
console.log("[ElevenLabs] Phone number configuration updated");
|
|
1451
1612
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Configure agent webhook for conversation-init events.
|
|
1616
|
+
* Matches C# ConfigureAgentWebhooksAsync.
|
|
1617
|
+
*/
|
|
1618
|
+
async configureAgentWebhooks(agentId, webhookBaseUrl) {
|
|
1619
|
+
const webhookUrl = `${webhookBaseUrl}/webhook/elevenlabs/conversation-init`;
|
|
1620
|
+
console.log(`[ElevenLabs] Configuring agent webhook`);
|
|
1621
|
+
const response = await fetch(`${ELEVENLABS_API_BASE}/convai/agents/${encodeURIComponent(agentId)}`, {
|
|
1622
|
+
method: "PATCH",
|
|
1623
|
+
headers: this.authHeaders,
|
|
1624
|
+
body: JSON.stringify({
|
|
1625
|
+
platform_settings: {
|
|
1626
|
+
workspace_overrides: {
|
|
1627
|
+
conversation_initiation_client_data_webhook: {
|
|
1628
|
+
url: webhookUrl,
|
|
1629
|
+
request_headers: {}
|
|
1630
|
+
}
|
|
1631
|
+
},
|
|
1632
|
+
overrides: {
|
|
1633
|
+
enable_conversation_initiation_client_data_from_webhook: true
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}),
|
|
1637
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1638
|
+
});
|
|
1639
|
+
if (!response.ok) {
|
|
1640
|
+
await response.text();
|
|
1641
|
+
console.error(`[ElevenLabs] Failed to configure agent webhooks: ${response.status}`);
|
|
1642
|
+
} else {
|
|
1643
|
+
console.log("[ElevenLabs] Agent webhooks configured successfully");
|
|
1455
1644
|
}
|
|
1456
|
-
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Map API response to our interface
|
|
1648
|
+
*/
|
|
1649
|
+
mapConversationResponse(data) {
|
|
1650
|
+
return {
|
|
1651
|
+
conversationId: data.conversation_id,
|
|
1652
|
+
agentId: data.agent_id,
|
|
1653
|
+
status: data.status,
|
|
1654
|
+
metadata: data.metadata ? {
|
|
1655
|
+
callDurationSeconds: data.metadata.call_duration_secs,
|
|
1656
|
+
startTime: data.metadata.start_time_unix_secs ? new Date(data.metadata.start_time_unix_secs * 1e3) : void 0,
|
|
1657
|
+
endTime: data.metadata.end_time_unix_secs ? new Date(data.metadata.end_time_unix_secs * 1e3) : void 0
|
|
1658
|
+
} : void 0,
|
|
1659
|
+
transcript: data.transcript?.map((t) => ({
|
|
1660
|
+
role: t.role,
|
|
1661
|
+
message: t.message,
|
|
1662
|
+
timeInCallSeconds: t.time_in_call_secs
|
|
1663
|
+
})),
|
|
1664
|
+
analysis: data.analysis ? {
|
|
1665
|
+
summary: data.analysis.transcript_summary,
|
|
1666
|
+
customData: data.analysis.custom_data
|
|
1667
|
+
} : void 0
|
|
1668
|
+
};
|
|
1457
1669
|
}
|
|
1458
1670
|
};
|
|
1671
|
+
function createElevenLabsClient(apiKey) {
|
|
1672
|
+
return new ElevenLabsClient(apiKey);
|
|
1673
|
+
}
|
|
1459
1674
|
|
|
1460
|
-
// ../runtime-shared/dist/
|
|
1461
|
-
|
|
1462
|
-
import path2 from "path";
|
|
1463
|
-
var VersionExtractor = class {
|
|
1675
|
+
// ../runtime-shared/dist/ai/elevenlabs/controller.js
|
|
1676
|
+
var ElevenLabsController = class {
|
|
1464
1677
|
/**
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1468
|
-
* @
|
|
1678
|
+
* Handle conversation initialization webhook
|
|
1679
|
+
* Called when an inbound call is received - allows customizing the conversation
|
|
1680
|
+
*
|
|
1681
|
+
* @param request - The conversation init request with caller info
|
|
1682
|
+
* @returns Response with dynamic variables and/or config overrides
|
|
1469
1683
|
*/
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1684
|
+
async handleConversationInit(request) {
|
|
1685
|
+
return {};
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Handle post-call webhook with transcript and analysis
|
|
1689
|
+
*
|
|
1690
|
+
* @param webhook - The post-call webhook payload
|
|
1691
|
+
*/
|
|
1692
|
+
async handlePostCall(webhook) {
|
|
1693
|
+
console.log("[ElevenLabs] Post-call webhook received:", {
|
|
1694
|
+
conversationId: webhook.data.conversation_id,
|
|
1695
|
+
agentId: webhook.data.agent_id,
|
|
1696
|
+
status: webhook.data.status,
|
|
1697
|
+
duration: webhook.data.metadata.call_duration_secs
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Handle post-call audio webhook with recording
|
|
1702
|
+
*
|
|
1703
|
+
* @param webhook - The audio webhook with base64-encoded MP3
|
|
1704
|
+
*/
|
|
1705
|
+
async handlePostCallAudio(webhook) {
|
|
1706
|
+
console.log("[ElevenLabs] Post-call audio received:", {
|
|
1707
|
+
conversationId: webhook.data.conversation_id,
|
|
1708
|
+
agentId: webhook.data.agent_id
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Handle call initiation failure webhook
|
|
1713
|
+
*
|
|
1714
|
+
* @param webhook - The failure webhook with error details
|
|
1715
|
+
*/
|
|
1716
|
+
async handleCallInitiationFailure(webhook) {
|
|
1717
|
+
console.error("[ElevenLabs] Call initiation failed:", {
|
|
1718
|
+
agentId: webhook.data.agent_id,
|
|
1719
|
+
customerNumber: webhook.data.customer_phone_number,
|
|
1720
|
+
reason: webhook.data.failure_reason
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Express route handler for conversation init endpoint
|
|
1725
|
+
* Mount at: POST /elevenlabs/conversation-init
|
|
1726
|
+
*/
|
|
1727
|
+
getConversationInitHandler() {
|
|
1728
|
+
return async (req, res) => {
|
|
1729
|
+
try {
|
|
1730
|
+
const request = req.body;
|
|
1731
|
+
const response = await this.handleConversationInit(request);
|
|
1732
|
+
res.json(response);
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
console.error("[ElevenLabs] Error in conversation init:", error);
|
|
1735
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1476
1736
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Express route handler for webhook endpoint
|
|
1741
|
+
* Mount at: POST /elevenlabs/webhook
|
|
1742
|
+
* Handles all webhook types based on the 'type' field
|
|
1743
|
+
*/
|
|
1744
|
+
getWebhookHandler() {
|
|
1745
|
+
return async (req, res) => {
|
|
1746
|
+
try {
|
|
1747
|
+
const webhook = req.body;
|
|
1748
|
+
switch (webhook.type) {
|
|
1749
|
+
case "post_call_transcription":
|
|
1750
|
+
await this.handlePostCall(webhook);
|
|
1751
|
+
break;
|
|
1752
|
+
case "post_call_audio":
|
|
1753
|
+
await this.handlePostCallAudio(webhook);
|
|
1754
|
+
break;
|
|
1755
|
+
case "call_initiation_failure":
|
|
1756
|
+
await this.handleCallInitiationFailure(webhook);
|
|
1757
|
+
break;
|
|
1758
|
+
default:
|
|
1759
|
+
console.warn("[ElevenLabs] Unknown webhook type:", webhook.type);
|
|
1484
1760
|
}
|
|
1761
|
+
res.status(200).send("OK");
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
console.error("[ElevenLabs] Error processing webhook:", error);
|
|
1764
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1485
1765
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
// ../runtime-shared/dist/ai/elevenlabs/autoconfigurator.js
|
|
1771
|
+
var FETCH_TIMEOUT_MS2 = 1e4;
|
|
1772
|
+
function maskPhone(phone) {
|
|
1773
|
+
if (phone.length <= 6)
|
|
1774
|
+
return "***";
|
|
1775
|
+
return phone.slice(0, 4) + "***" + phone.slice(-4);
|
|
1776
|
+
}
|
|
1777
|
+
async function tryAutoConfigureAsync(webhookUrl, functionName, sinchPhoneNumber) {
|
|
1778
|
+
const autoConfig = process.env.ELEVENLABS_AUTO_CONFIGURE;
|
|
1779
|
+
if (!autoConfig || autoConfig.toLowerCase() !== "true") {
|
|
1780
|
+
return null;
|
|
1781
|
+
}
|
|
1782
|
+
console.log("[ElevenLabs] Auto-configuration is enabled");
|
|
1783
|
+
const agentId = process.env.ELEVENLABS_AGENT_ID;
|
|
1784
|
+
const apiKey = process.env.ELEVENLABS_API_KEY;
|
|
1785
|
+
if (!agentId) {
|
|
1786
|
+
console.warn("[ElevenLabs] Auto-configuration skipped: ELEVENLABS_AGENT_ID not set. Please configure your agent ID in environment variables.");
|
|
1787
|
+
return null;
|
|
1788
|
+
}
|
|
1789
|
+
if (!apiKey) {
|
|
1790
|
+
console.warn("[ElevenLabs] Auto-configuration skipped: ELEVENLABS_API_KEY not set. Please store your API key using: sinch secrets add ELEVENLABS_API_KEY sk_...");
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
let phoneNumber = sinchPhoneNumber ?? process.env.SINCH_PHONE_NUMBER;
|
|
1794
|
+
if (!phoneNumber) {
|
|
1795
|
+
console.log("[ElevenLabs] SINCH_PHONE_NUMBER not configured, attempting auto-discovery from Voice API...");
|
|
1796
|
+
phoneNumber = await autoDiscoverPhoneNumber();
|
|
1797
|
+
}
|
|
1798
|
+
if (!phoneNumber) {
|
|
1799
|
+
console.warn("[ElevenLabs] Auto-configuration skipped: Sinch phone number not provided and auto-discovery failed. Please set SINCH_PHONE_NUMBER or ensure Voice API credentials are configured.");
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
if (!webhookUrl) {
|
|
1803
|
+
console.warn("[ElevenLabs] Auto-configuration skipped: Webhook URL not provided.");
|
|
1804
|
+
return null;
|
|
1805
|
+
}
|
|
1806
|
+
try {
|
|
1807
|
+
new URL(webhookUrl);
|
|
1808
|
+
} catch {
|
|
1809
|
+
console.warn("[ElevenLabs] Auto-configuration skipped: Webhook URL is not a valid URL.");
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
const voiceAppKey = process.env.VOICE_APPLICATION_KEY || "";
|
|
1813
|
+
const voiceAppSecret = process.env.VOICE_APPLICATION_SECRET || "";
|
|
1814
|
+
const safeName = functionName ? functionName.slice(0, 100) : "Sinch Function";
|
|
1815
|
+
try {
|
|
1816
|
+
console.log(`[ElevenLabs] Starting auto-configuration: Agent=${agentId}, Number=${maskPhone(phoneNumber)}`);
|
|
1817
|
+
const client = new ElevenLabsClient(apiKey);
|
|
1818
|
+
const result = await client.autoConfigureAsync({
|
|
1819
|
+
agentId,
|
|
1820
|
+
phoneNumber,
|
|
1821
|
+
sipAddress: "use1.vp.sip.sinch.com",
|
|
1822
|
+
sipUsername: voiceAppKey,
|
|
1823
|
+
sipPassword: voiceAppSecret,
|
|
1824
|
+
webhookBaseUrl: webhookUrl,
|
|
1825
|
+
functionName: safeName
|
|
1826
|
+
});
|
|
1827
|
+
if (result.success) {
|
|
1828
|
+
ElevenLabsState.setConfigured({
|
|
1829
|
+
phoneNumberId: result.phoneNumberId,
|
|
1830
|
+
phoneNumber: result.phoneNumber,
|
|
1831
|
+
agentId,
|
|
1832
|
+
sipAddress: result.sipAddress
|
|
1833
|
+
});
|
|
1834
|
+
console.log(`[ElevenLabs] Auto-configuration completed! Phone: ${maskPhone(result.phoneNumber ?? "")}, SIP: ${result.sipAddress}`);
|
|
1491
1835
|
}
|
|
1836
|
+
return result;
|
|
1837
|
+
} catch (error) {
|
|
1838
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1839
|
+
console.error(`[ElevenLabs] Auto-configuration failed: ${message}. The function will start but ElevenLabs integration may not work.`);
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
async function autoDiscoverPhoneNumber() {
|
|
1844
|
+
const appKey = process.env.VOICE_APPLICATION_KEY || "";
|
|
1845
|
+
const appSecret = process.env.VOICE_APPLICATION_SECRET || "";
|
|
1846
|
+
if (!appKey || !appSecret) {
|
|
1847
|
+
console.warn("[ElevenLabs] Voice client not available for phone number auto-discovery. Ensure VOICE_APPLICATION_KEY and VOICE_APPLICATION_SECRET are configured.");
|
|
1848
|
+
return void 0;
|
|
1492
1849
|
}
|
|
1850
|
+
try {
|
|
1851
|
+
const auth = Buffer.from(`${appKey}:${appSecret}`).toString("base64");
|
|
1852
|
+
const configResponse = await fetch(`https://callingapi.sinch.com/v1/configuration/numbers/`, {
|
|
1853
|
+
headers: { Authorization: `Basic ${auth}` },
|
|
1854
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS2)
|
|
1855
|
+
});
|
|
1856
|
+
if (!configResponse.ok) {
|
|
1857
|
+
console.warn(`[ElevenLabs] Failed to query Voice API for phone numbers: ${configResponse.status}`);
|
|
1858
|
+
return void 0;
|
|
1859
|
+
}
|
|
1860
|
+
const data = await configResponse.json();
|
|
1861
|
+
const numbers = (data.numbers || []).filter((n) => n.applicationkey === appKey).map((n) => n.number).filter(Boolean);
|
|
1862
|
+
if (numbers.length > 0) {
|
|
1863
|
+
console.log(`[ElevenLabs] Auto-discovered ${numbers.length} phone number(s), using first`);
|
|
1864
|
+
return numbers[0];
|
|
1865
|
+
}
|
|
1866
|
+
console.warn("[ElevenLabs] No phone numbers found for the configured Voice application");
|
|
1867
|
+
return void 0;
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1870
|
+
console.error(`[ElevenLabs] Failed to auto-discover phone number: ${message}`);
|
|
1871
|
+
return void 0;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
var ElevenLabsAutoConfigurator = class {
|
|
1875
|
+
static tryAutoConfigureAsync = tryAutoConfigureAsync;
|
|
1493
1876
|
};
|
|
1494
1877
|
|
|
1495
|
-
// ../runtime-shared/dist/
|
|
1496
|
-
var
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1878
|
+
// ../runtime-shared/dist/conversation/message-builder.js
|
|
1879
|
+
var ConversationMessageBuilder = class {
|
|
1880
|
+
config;
|
|
1881
|
+
appId;
|
|
1882
|
+
channel;
|
|
1883
|
+
identity;
|
|
1884
|
+
conversationId;
|
|
1885
|
+
messageText;
|
|
1886
|
+
/**
|
|
1887
|
+
* Create a new builder with configuration
|
|
1888
|
+
*/
|
|
1889
|
+
constructor(config = {}) {
|
|
1890
|
+
this.config = config;
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Pre-fill builder from an inbound message (for replies)
|
|
1894
|
+
*/
|
|
1895
|
+
fromInbound(inbound) {
|
|
1896
|
+
this.appId = inbound.app_id;
|
|
1897
|
+
this.channel = inbound.message?.channel_identity?.channel;
|
|
1898
|
+
this.identity = inbound.message?.channel_identity?.identity;
|
|
1899
|
+
this.conversationId = inbound.message?.conversation_id;
|
|
1900
|
+
return this;
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Set the app ID (defaults to CONVERSATION_APP_ID from config)
|
|
1904
|
+
*/
|
|
1905
|
+
setAppId(appId) {
|
|
1906
|
+
this.appId = appId;
|
|
1907
|
+
return this;
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Set the recipient channel and identity
|
|
1911
|
+
*
|
|
1912
|
+
* @param channel - Channel name (SMS, WHATSAPP, MESSENGER, etc.)
|
|
1913
|
+
* @param identity - Channel-specific identity (phone number, PSID, etc.)
|
|
1914
|
+
*/
|
|
1915
|
+
to(channel, identity) {
|
|
1916
|
+
this.channel = channel;
|
|
1917
|
+
this.identity = identity;
|
|
1918
|
+
return this;
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Set the conversation ID (for non-DISPATCH mode apps)
|
|
1922
|
+
*/
|
|
1923
|
+
setConversationId(conversationId) {
|
|
1924
|
+
this.conversationId = conversationId;
|
|
1925
|
+
return this;
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Set a text message
|
|
1929
|
+
*/
|
|
1930
|
+
text(text) {
|
|
1931
|
+
this.messageText = text;
|
|
1932
|
+
return this;
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Build the SendMessageRequest.
|
|
1936
|
+
* Automatically sets SMS_SENDER from config if sending to SMS channel.
|
|
1937
|
+
*
|
|
1938
|
+
* @returns A SendMessageRequest ready to send via context.conversation.messages.send()
|
|
1939
|
+
*/
|
|
1940
|
+
build() {
|
|
1941
|
+
const appId = this.appId || this.config.CONVERSATION_APP_ID;
|
|
1942
|
+
if (!appId) {
|
|
1943
|
+
throw new Error("AppId required - use setAppId() or set CONVERSATION_APP_ID in config");
|
|
1944
|
+
}
|
|
1945
|
+
if (!this.channel || !this.identity) {
|
|
1946
|
+
throw new Error("Recipient required - use to() or fromInbound()");
|
|
1947
|
+
}
|
|
1948
|
+
if (!this.messageText) {
|
|
1949
|
+
throw new Error("Message required - use text()");
|
|
1950
|
+
}
|
|
1951
|
+
const request = {
|
|
1952
|
+
app_id: appId,
|
|
1953
|
+
message: {
|
|
1954
|
+
text_message: {
|
|
1955
|
+
text: this.messageText
|
|
1956
|
+
}
|
|
1957
|
+
},
|
|
1958
|
+
recipient: {
|
|
1959
|
+
identified_by: {
|
|
1960
|
+
channel_identities: [
|
|
1961
|
+
{
|
|
1962
|
+
channel: this.channel,
|
|
1963
|
+
identity: this.identity
|
|
1964
|
+
}
|
|
1965
|
+
]
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
if (this.channel.toUpperCase() === "SMS") {
|
|
1970
|
+
const smsSender = this.config.FROM_NUMBER;
|
|
1971
|
+
if (smsSender) {
|
|
1972
|
+
request.channel_properties = {
|
|
1973
|
+
SMS_SENDER: smsSender
|
|
1528
1974
|
};
|
|
1529
|
-
} catch (error) {
|
|
1530
|
-
console.warn("HTML template not found, returning JSON");
|
|
1531
1975
|
}
|
|
1532
1976
|
}
|
|
1533
|
-
return
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1977
|
+
return request;
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
function createMessageBuilder(config = {}) {
|
|
1981
|
+
return new ConversationMessageBuilder(config);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// ../runtime-shared/dist/conversation/message-helpers.js
|
|
1985
|
+
function textReply(inbound, text, smsSender) {
|
|
1986
|
+
const channelIdentity = inbound.message?.channel_identity;
|
|
1987
|
+
if (!channelIdentity) {
|
|
1988
|
+
throw new Error("Inbound message has no channel identity");
|
|
1989
|
+
}
|
|
1990
|
+
const channel = channelIdentity.channel;
|
|
1991
|
+
const identity = channelIdentity.identity;
|
|
1992
|
+
if (!channel || !identity) {
|
|
1993
|
+
throw new Error("Inbound message has no channel or identity");
|
|
1994
|
+
}
|
|
1995
|
+
return createChannelMessage(inbound.app_id, channel, identity, text, smsSender);
|
|
1996
|
+
}
|
|
1997
|
+
function createSms(appId, phoneNumber, text, smsSender) {
|
|
1998
|
+
return createChannelMessage(appId, "SMS", phoneNumber, text, smsSender);
|
|
1999
|
+
}
|
|
2000
|
+
function createWhatsApp(appId, phoneNumber, text) {
|
|
2001
|
+
return createChannelMessage(appId, "WHATSAPP", phoneNumber, text);
|
|
2002
|
+
}
|
|
2003
|
+
function createMessenger(appId, psid, text) {
|
|
2004
|
+
return createChannelMessage(appId, "MESSENGER", psid, text);
|
|
2005
|
+
}
|
|
2006
|
+
function createChannelMessage(appId, channel, identity, text, smsSender) {
|
|
2007
|
+
if (!appId)
|
|
2008
|
+
throw new Error("appId is required");
|
|
2009
|
+
if (!channel)
|
|
2010
|
+
throw new Error("channel is required");
|
|
2011
|
+
if (!identity)
|
|
2012
|
+
throw new Error("identity is required");
|
|
2013
|
+
if (!text)
|
|
2014
|
+
throw new Error("text is required");
|
|
2015
|
+
const request = {
|
|
2016
|
+
app_id: appId,
|
|
2017
|
+
message: {
|
|
2018
|
+
text_message: {
|
|
2019
|
+
text
|
|
2020
|
+
}
|
|
2021
|
+
},
|
|
2022
|
+
recipient: {
|
|
2023
|
+
identified_by: {
|
|
2024
|
+
channel_identities: [
|
|
2025
|
+
{
|
|
2026
|
+
channel,
|
|
2027
|
+
identity
|
|
2028
|
+
}
|
|
2029
|
+
]
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
if (channel.toUpperCase() === "SMS" && smsSender) {
|
|
2034
|
+
request.channel_properties = {
|
|
2035
|
+
SMS_SENDER: smsSender
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
return request;
|
|
2039
|
+
}
|
|
2040
|
+
var ConversationMessage = {
|
|
2041
|
+
textReply,
|
|
2042
|
+
createSms,
|
|
2043
|
+
createWhatsApp,
|
|
2044
|
+
createMessenger,
|
|
2045
|
+
createChannelMessage
|
|
2046
|
+
};
|
|
2047
|
+
|
|
2048
|
+
// ../runtime-shared/dist/conversation/controller.js
|
|
2049
|
+
var ConversationController = class {
|
|
2050
|
+
conversation;
|
|
2051
|
+
config;
|
|
2052
|
+
constructor(conversation, config = {}) {
|
|
2053
|
+
this.conversation = conversation;
|
|
2054
|
+
this.config = config;
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* MESSAGE_INBOUND - Called when a user sends a message.
|
|
2058
|
+
* Override this to handle incoming messages from users.
|
|
2059
|
+
*/
|
|
2060
|
+
async handleMessageInbound(event) {
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* MESSAGE_DELIVERY - Called when message delivery status updates.
|
|
2064
|
+
* Override this to track delivery receipts.
|
|
2065
|
+
*/
|
|
2066
|
+
async handleMessageDelivery(event) {
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* EVENT_INBOUND - Called for events like composing indicators.
|
|
2070
|
+
* Override this to handle typing indicators, read receipts, etc.
|
|
2071
|
+
*/
|
|
2072
|
+
async handleEventInbound(event) {
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* CONVERSATION_START - Called when a conversation begins.
|
|
2076
|
+
* Override this to handle conversation initialization.
|
|
2077
|
+
*/
|
|
2078
|
+
async handleConversationStart(event) {
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* CONVERSATION_STOP - Called when a conversation ends.
|
|
2082
|
+
* Override this to handle conversation cleanup.
|
|
2083
|
+
*/
|
|
2084
|
+
async handleConversationStop(event) {
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Gets the sender number from configuration (FROM_NUMBER).
|
|
2088
|
+
* Used for setting SMS_SENDER channel property when sending to SMS channel.
|
|
2089
|
+
*/
|
|
2090
|
+
get fromNumber() {
|
|
2091
|
+
return this.config.FROM_NUMBER;
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Create a text reply to an inbound message.
|
|
2095
|
+
* Automatically sets SMS_SENDER from FROM_NUMBER config.
|
|
2096
|
+
*/
|
|
2097
|
+
reply(inbound, text) {
|
|
2098
|
+
return textReply(inbound, text, this.fromNumber);
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Create a ConversationMessageBuilder for building messages with full control.
|
|
2102
|
+
*/
|
|
2103
|
+
createMessage() {
|
|
2104
|
+
return new ConversationMessageBuilder(this.config);
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Create a ConversationMessageBuilder pre-filled from an inbound message.
|
|
2108
|
+
*/
|
|
2109
|
+
createReply(inbound) {
|
|
2110
|
+
return new ConversationMessageBuilder(this.config).fromInbound(inbound);
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Express route handler for the webhook endpoint.
|
|
2114
|
+
* Mount at: POST /conversation
|
|
2115
|
+
*
|
|
2116
|
+
* Routes to appropriate handler based on event type.
|
|
2117
|
+
* Uses manual JSON parsing as workaround for SDK issues with extra fields.
|
|
2118
|
+
*/
|
|
2119
|
+
getWebhookHandler() {
|
|
2120
|
+
return async (req, res) => {
|
|
2121
|
+
try {
|
|
2122
|
+
const body = req.body;
|
|
2123
|
+
const verbose = this.config.VERBOSE === "true";
|
|
2124
|
+
if (!body) {
|
|
2125
|
+
res.status(400).json({ error: "Empty request body" });
|
|
2126
|
+
return;
|
|
1548
2127
|
}
|
|
2128
|
+
if (!this.conversation) {
|
|
2129
|
+
console.error("[Conversation] Client not configured");
|
|
2130
|
+
res.status(500).json({ error: "Conversation API not configured" });
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
if (body.message?.direction === "TO_APP" && body.message?.contact_message) {
|
|
2134
|
+
if (verbose) {
|
|
2135
|
+
console.log("[Conversation] MESSAGE_INBOUND event detected");
|
|
2136
|
+
}
|
|
2137
|
+
await this.handleMessageInbound(body);
|
|
2138
|
+
res.status(200).send("OK");
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
if (body.message_delivery_report) {
|
|
2142
|
+
if (verbose) {
|
|
2143
|
+
console.log("[Conversation] MESSAGE_DELIVERY event detected");
|
|
2144
|
+
}
|
|
2145
|
+
await this.handleMessageDelivery(body);
|
|
2146
|
+
res.status(200).send("OK");
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
if (body.event?.contact_event) {
|
|
2150
|
+
if (verbose) {
|
|
2151
|
+
console.log("[Conversation] EVENT_INBOUND event detected");
|
|
2152
|
+
}
|
|
2153
|
+
await this.handleEventInbound(body);
|
|
2154
|
+
res.status(200).send("OK");
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
if (body.conversation_event) {
|
|
2158
|
+
const eventType = body.conversation_event.type;
|
|
2159
|
+
if (eventType === "CONVERSATION_START") {
|
|
2160
|
+
if (verbose) {
|
|
2161
|
+
console.log("[Conversation] CONVERSATION_START event detected");
|
|
2162
|
+
}
|
|
2163
|
+
await this.handleConversationStart(body);
|
|
2164
|
+
} else if (eventType === "CONVERSATION_STOP") {
|
|
2165
|
+
if (verbose) {
|
|
2166
|
+
console.log("[Conversation] CONVERSATION_STOP event detected");
|
|
2167
|
+
}
|
|
2168
|
+
await this.handleConversationStop(body);
|
|
2169
|
+
}
|
|
2170
|
+
res.status(200).send("OK");
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
if (verbose) {
|
|
2174
|
+
console.log("[Conversation] Unknown event type, ignoring");
|
|
2175
|
+
}
|
|
2176
|
+
res.status(200).json({ status: "ignored", reason: "unknown_event_type" });
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
console.error("[Conversation] Error processing webhook:", error);
|
|
2179
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1549
2180
|
}
|
|
1550
2181
|
};
|
|
1551
2182
|
}
|
|
1552
2183
|
};
|
|
1553
2184
|
|
|
2185
|
+
// ../runtime-shared/dist/conversation/extensions.js
|
|
2186
|
+
function getText(event) {
|
|
2187
|
+
return event.message?.contact_message?.text_message?.text;
|
|
2188
|
+
}
|
|
2189
|
+
function getMedia(event) {
|
|
2190
|
+
return event.message?.contact_message?.media_message;
|
|
2191
|
+
}
|
|
2192
|
+
function getPostbackData(event) {
|
|
2193
|
+
return event.message?.contact_message?.choice_response_message?.postback_data;
|
|
2194
|
+
}
|
|
2195
|
+
function getContactId(event) {
|
|
2196
|
+
return event.message?.contact_id;
|
|
2197
|
+
}
|
|
2198
|
+
function getConversationId(event) {
|
|
2199
|
+
return event.message?.conversation_id;
|
|
2200
|
+
}
|
|
2201
|
+
function getChannel(event) {
|
|
2202
|
+
return event.message?.channel_identity?.channel;
|
|
2203
|
+
}
|
|
2204
|
+
function getIdentity(event) {
|
|
2205
|
+
return event.message?.channel_identity?.identity;
|
|
2206
|
+
}
|
|
2207
|
+
function getTo(event) {
|
|
2208
|
+
return event.message?.sender_id;
|
|
2209
|
+
}
|
|
2210
|
+
function getLocation(event) {
|
|
2211
|
+
return event.message?.contact_message?.location_message;
|
|
2212
|
+
}
|
|
2213
|
+
function isTextMessage(event) {
|
|
2214
|
+
return event.message?.contact_message?.text_message !== void 0;
|
|
2215
|
+
}
|
|
2216
|
+
function isMediaMessage(event) {
|
|
2217
|
+
return event.message?.contact_message?.media_message !== void 0;
|
|
2218
|
+
}
|
|
2219
|
+
function isPostback(event) {
|
|
2220
|
+
return event.message?.contact_message?.choice_response_message !== void 0;
|
|
2221
|
+
}
|
|
2222
|
+
function createMessageHelper(event) {
|
|
2223
|
+
return {
|
|
2224
|
+
getText: () => getText(event),
|
|
2225
|
+
getMedia: () => getMedia(event),
|
|
2226
|
+
getPostbackData: () => getPostbackData(event),
|
|
2227
|
+
getContactId: () => getContactId(event),
|
|
2228
|
+
getConversationId: () => getConversationId(event),
|
|
2229
|
+
getChannel: () => getChannel(event),
|
|
2230
|
+
getIdentity: () => getIdentity(event),
|
|
2231
|
+
getTo: () => getTo(event),
|
|
2232
|
+
getLocation: () => getLocation(event),
|
|
2233
|
+
isTextMessage: () => isTextMessage(event),
|
|
2234
|
+
isMediaMessage: () => isMediaMessage(event),
|
|
2235
|
+
isPostback: () => isPostback(event)
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// ../runtime-shared/dist/voice/helpers.js
|
|
2240
|
+
var COUNTRY_CODES = {
|
|
2241
|
+
"+1": { code: "US", format: (n) => `(${n.slice(0, 3)}) ${n.slice(3, 6)}-${n.slice(6)}` },
|
|
2242
|
+
"+44": { code: "GB", format: (n) => `0${n.slice(0, 4)} ${n.slice(4)}` },
|
|
2243
|
+
"+46": {
|
|
2244
|
+
code: "SE",
|
|
2245
|
+
format: (n) => `0${n.slice(0, 2)}-${n.slice(2, 5)} ${n.slice(5, 7)} ${n.slice(7)}`
|
|
2246
|
+
},
|
|
2247
|
+
"+49": { code: "DE", format: (n) => `0${n.slice(0, 3)} ${n.slice(3)}` },
|
|
2248
|
+
"+33": {
|
|
2249
|
+
code: "FR",
|
|
2250
|
+
format: (n) => `0${n.slice(0, 1)} ${n.slice(1, 3)} ${n.slice(3, 5)} ${n.slice(5, 7)} ${n.slice(7)}`
|
|
2251
|
+
},
|
|
2252
|
+
"+61": { code: "AU", format: (n) => `0${n.slice(0, 1)} ${n.slice(1, 5)} ${n.slice(5)}` }
|
|
2253
|
+
};
|
|
2254
|
+
function toLocalPhoneNumber(phoneNumber, defaultRegion = "US") {
|
|
2255
|
+
if (!phoneNumber || !phoneNumber.startsWith("+")) {
|
|
2256
|
+
return phoneNumber;
|
|
2257
|
+
}
|
|
2258
|
+
const sortedCodes = Object.keys(COUNTRY_CODES).sort((a, b) => b.length - a.length);
|
|
2259
|
+
for (const prefix of sortedCodes) {
|
|
2260
|
+
if (phoneNumber.startsWith(prefix)) {
|
|
2261
|
+
const national = phoneNumber.slice(prefix.length);
|
|
2262
|
+
try {
|
|
2263
|
+
return COUNTRY_CODES[prefix].format(national);
|
|
2264
|
+
} catch {
|
|
2265
|
+
return phoneNumber;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
return phoneNumber.slice(1);
|
|
2270
|
+
}
|
|
2271
|
+
function formatPhoneNumber(phoneNumber) {
|
|
2272
|
+
const cleaned = phoneNumber.replace(/[^\d+]/g, "");
|
|
2273
|
+
if (cleaned.startsWith("+")) {
|
|
2274
|
+
return toLocalPhoneNumber(cleaned);
|
|
2275
|
+
}
|
|
2276
|
+
if (cleaned.length === 10) {
|
|
2277
|
+
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
|
|
2278
|
+
}
|
|
2279
|
+
return phoneNumber;
|
|
2280
|
+
}
|
|
2281
|
+
var VoiceErrorHelper = {
|
|
2282
|
+
/**
|
|
2283
|
+
* Create a standard error response that plays a message and hangs up
|
|
2284
|
+
*
|
|
2285
|
+
* @param message - Error message to speak
|
|
2286
|
+
* @param locale - Language locale (default: "en-US")
|
|
2287
|
+
* @returns SVAML response object
|
|
2288
|
+
*/
|
|
2289
|
+
createErrorResponse(message, locale = "en-US") {
|
|
2290
|
+
return new IceSvamlBuilder().say(message, locale).hangup().build();
|
|
2291
|
+
},
|
|
2292
|
+
/**
|
|
2293
|
+
* Create a service unavailable response
|
|
2294
|
+
*
|
|
2295
|
+
* @param locale - Language locale (default: "en-US")
|
|
2296
|
+
* @returns SVAML response object
|
|
2297
|
+
*/
|
|
2298
|
+
serviceUnavailable(locale = "en-US") {
|
|
2299
|
+
return this.createErrorResponse("We're sorry, our service is temporarily unavailable. Please try again later.", locale);
|
|
2300
|
+
},
|
|
2301
|
+
/**
|
|
2302
|
+
* Create an invalid input response for PIE handlers
|
|
2303
|
+
*
|
|
2304
|
+
* @param locale - Language locale (default: "en-US")
|
|
2305
|
+
* @returns SVAML response object
|
|
2306
|
+
*/
|
|
2307
|
+
invalidInput(locale = "en-US") {
|
|
2308
|
+
return new PieSvamlBuilder().say("Invalid selection. Please try again.", locale).continue().build();
|
|
2309
|
+
},
|
|
2310
|
+
/**
|
|
2311
|
+
* Create a timeout response for PIE handlers
|
|
2312
|
+
*
|
|
2313
|
+
* @param locale - Language locale (default: "en-US")
|
|
2314
|
+
* @returns SVAML response object
|
|
2315
|
+
*/
|
|
2316
|
+
timeout(locale = "en-US") {
|
|
2317
|
+
return new PieSvamlBuilder().say("We did not receive a response. Please try again.", locale).continue().build();
|
|
2318
|
+
},
|
|
2319
|
+
/**
|
|
2320
|
+
* Create a goodbye response that thanks the caller and hangs up
|
|
2321
|
+
*
|
|
2322
|
+
* @param locale - Language locale (default: "en-US")
|
|
2323
|
+
* @returns SVAML response object
|
|
2324
|
+
*/
|
|
2325
|
+
goodbye(locale = "en-US") {
|
|
2326
|
+
return new IceSvamlBuilder().say("Thank you for calling. Goodbye!", locale).hangup().build();
|
|
2327
|
+
}
|
|
2328
|
+
};
|
|
2329
|
+
function formatDuration(seconds) {
|
|
2330
|
+
if (seconds < 60) {
|
|
2331
|
+
return `${Math.round(seconds)}s`;
|
|
2332
|
+
}
|
|
2333
|
+
const minutes = Math.floor(seconds / 60);
|
|
2334
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
2335
|
+
if (remainingSeconds === 0) {
|
|
2336
|
+
return `${minutes}m`;
|
|
2337
|
+
}
|
|
2338
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
2339
|
+
}
|
|
2340
|
+
function extractCallerNumber(cli) {
|
|
2341
|
+
if (!cli)
|
|
2342
|
+
return void 0;
|
|
2343
|
+
return cli.replace(/[^\d+]/g, "");
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// ../runtime-shared/dist/utils/templateRender.js
|
|
2347
|
+
import fs from "fs";
|
|
2348
|
+
import path from "path";
|
|
2349
|
+
|
|
2350
|
+
// ../runtime-shared/dist/utils/versionExtractor.js
|
|
2351
|
+
import fs2 from "fs";
|
|
2352
|
+
import path2 from "path";
|
|
2353
|
+
|
|
1554
2354
|
// ../runtime-shared/dist/utils/functionLoader.js
|
|
1555
2355
|
import path3 from "path";
|
|
1556
2356
|
|
|
@@ -1651,6 +2451,69 @@ function createCacheClient(_projectId, _functionName) {
|
|
|
1651
2451
|
return new LocalCache();
|
|
1652
2452
|
}
|
|
1653
2453
|
|
|
2454
|
+
// src/storage/local.ts
|
|
2455
|
+
import * as fs3 from "fs/promises";
|
|
2456
|
+
import * as path4 from "path";
|
|
2457
|
+
var LocalStorage = class {
|
|
2458
|
+
baseDir;
|
|
2459
|
+
constructor(baseDir) {
|
|
2460
|
+
this.baseDir = baseDir ?? path4.join(process.cwd(), "storage");
|
|
2461
|
+
}
|
|
2462
|
+
resolvePath(key) {
|
|
2463
|
+
const sanitized = key.replace(/^\/+/, "").replace(/\.\./g, "_");
|
|
2464
|
+
return path4.join(this.baseDir, sanitized);
|
|
2465
|
+
}
|
|
2466
|
+
async write(key, data) {
|
|
2467
|
+
const filePath = this.resolvePath(key);
|
|
2468
|
+
await fs3.mkdir(path4.dirname(filePath), { recursive: true });
|
|
2469
|
+
await fs3.writeFile(filePath, data);
|
|
2470
|
+
}
|
|
2471
|
+
async read(key) {
|
|
2472
|
+
const filePath = this.resolvePath(key);
|
|
2473
|
+
return fs3.readFile(filePath);
|
|
2474
|
+
}
|
|
2475
|
+
async list(prefix) {
|
|
2476
|
+
const results = [];
|
|
2477
|
+
await this.walkDir(this.baseDir, "", results);
|
|
2478
|
+
if (prefix) {
|
|
2479
|
+
return results.filter((f) => f.startsWith(prefix));
|
|
2480
|
+
}
|
|
2481
|
+
return results;
|
|
2482
|
+
}
|
|
2483
|
+
async exists(key) {
|
|
2484
|
+
const filePath = this.resolvePath(key);
|
|
2485
|
+
try {
|
|
2486
|
+
await fs3.access(filePath);
|
|
2487
|
+
return true;
|
|
2488
|
+
} catch {
|
|
2489
|
+
return false;
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
async delete(key) {
|
|
2493
|
+
const filePath = this.resolvePath(key);
|
|
2494
|
+
await fs3.rm(filePath, { force: true });
|
|
2495
|
+
}
|
|
2496
|
+
async walkDir(dir, relative, results) {
|
|
2497
|
+
let entries;
|
|
2498
|
+
try {
|
|
2499
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
2500
|
+
} catch {
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
for (const entry of entries) {
|
|
2504
|
+
const rel = relative ? `${relative}/${entry.name}` : entry.name;
|
|
2505
|
+
if (entry.isDirectory()) {
|
|
2506
|
+
await this.walkDir(path4.join(dir, entry.name), rel, results);
|
|
2507
|
+
} else {
|
|
2508
|
+
results.push(rel);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
function createStorageClient(baseDir) {
|
|
2514
|
+
return new LocalStorage(baseDir);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
1654
2517
|
// src/tunnel/index.ts
|
|
1655
2518
|
import WebSocket from "ws";
|
|
1656
2519
|
import axios from "axios";
|
|
@@ -2042,9 +2905,242 @@ function getTunnelClient(localPort = 3e3) {
|
|
|
2042
2905
|
}
|
|
2043
2906
|
|
|
2044
2907
|
// src/secrets/index.ts
|
|
2045
|
-
import
|
|
2046
|
-
import
|
|
2908
|
+
import fs4 from "fs";
|
|
2909
|
+
import path5 from "path";
|
|
2047
2910
|
import os from "os";
|
|
2911
|
+
|
|
2912
|
+
// src/secrets/keychain.ts
|
|
2913
|
+
import { execFile, spawn } from "child_process";
|
|
2914
|
+
import { promisify } from "util";
|
|
2915
|
+
var execFileAsync = promisify(execFile);
|
|
2916
|
+
function b64(value) {
|
|
2917
|
+
return Buffer.from(value, "utf8").toString("base64");
|
|
2918
|
+
}
|
|
2919
|
+
function psParam(name, value) {
|
|
2920
|
+
return `$${name} = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${b64(value)}'));`;
|
|
2921
|
+
}
|
|
2922
|
+
var WIN32_CRED_READ_SCRIPT = `
|
|
2923
|
+
Add-Type -TypeDefinition @'
|
|
2924
|
+
using System;
|
|
2925
|
+
using System.Runtime.InteropServices;
|
|
2926
|
+
public class CredManager {
|
|
2927
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
|
|
2928
|
+
private struct CREDENTIAL {
|
|
2929
|
+
public int Flags;
|
|
2930
|
+
public int Type;
|
|
2931
|
+
public IntPtr TargetName;
|
|
2932
|
+
public IntPtr Comment;
|
|
2933
|
+
public long LastWritten;
|
|
2934
|
+
public int CredentialBlobSize;
|
|
2935
|
+
public IntPtr CredentialBlob;
|
|
2936
|
+
public int Persist;
|
|
2937
|
+
public int AttributeCount;
|
|
2938
|
+
public IntPtr Attributes;
|
|
2939
|
+
public IntPtr TargetAlias;
|
|
2940
|
+
public IntPtr UserName;
|
|
2941
|
+
}
|
|
2942
|
+
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
|
2943
|
+
private static extern bool CredReadW(string target, int type, int flags, out IntPtr cred);
|
|
2944
|
+
[DllImport("advapi32.dll")]
|
|
2945
|
+
private static extern void CredFree(IntPtr cred);
|
|
2946
|
+
public static string Read(string target) {
|
|
2947
|
+
IntPtr credPtr;
|
|
2948
|
+
if (!CredReadW(target, 1, 0, out credPtr)) return null;
|
|
2949
|
+
try {
|
|
2950
|
+
CREDENTIAL c = (CREDENTIAL)Marshal.PtrToStructure(credPtr, typeof(CREDENTIAL));
|
|
2951
|
+
if (c.CredentialBlobSize > 0 && c.CredentialBlob != IntPtr.Zero)
|
|
2952
|
+
return Marshal.PtrToStringUni(c.CredentialBlob, c.CredentialBlobSize / 2);
|
|
2953
|
+
return "";
|
|
2954
|
+
} finally { CredFree(credPtr); }
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
'@
|
|
2958
|
+
$r = [CredManager]::Read($target)
|
|
2959
|
+
if ($r -ne $null) { [Console]::Write($r) }
|
|
2960
|
+
else { exit 1 }
|
|
2961
|
+
`;
|
|
2962
|
+
var WIN32_CRED_WRITE_SCRIPT = `
|
|
2963
|
+
Add-Type -TypeDefinition @'
|
|
2964
|
+
using System;
|
|
2965
|
+
using System.Runtime.InteropServices;
|
|
2966
|
+
using System.Text;
|
|
2967
|
+
public class CredWriter {
|
|
2968
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
|
|
2969
|
+
private struct CREDENTIAL {
|
|
2970
|
+
public int Flags;
|
|
2971
|
+
public int Type;
|
|
2972
|
+
public string TargetName;
|
|
2973
|
+
public string Comment;
|
|
2974
|
+
public long LastWritten;
|
|
2975
|
+
public int CredentialBlobSize;
|
|
2976
|
+
public IntPtr CredentialBlob;
|
|
2977
|
+
public int Persist;
|
|
2978
|
+
public int AttributeCount;
|
|
2979
|
+
public IntPtr Attributes;
|
|
2980
|
+
public string TargetAlias;
|
|
2981
|
+
public string UserName;
|
|
2982
|
+
}
|
|
2983
|
+
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
|
2984
|
+
private static extern bool CredWriteW(ref CREDENTIAL cred, int flags);
|
|
2985
|
+
public static bool Write(string target, string password) {
|
|
2986
|
+
byte[] blob = Encoding.Unicode.GetBytes(password);
|
|
2987
|
+
CREDENTIAL c = new CREDENTIAL();
|
|
2988
|
+
c.Type = 1;
|
|
2989
|
+
c.TargetName = target;
|
|
2990
|
+
c.CredentialBlobSize = blob.Length;
|
|
2991
|
+
c.CredentialBlob = Marshal.AllocHGlobal(blob.Length);
|
|
2992
|
+
Marshal.Copy(blob, 0, c.CredentialBlob, blob.Length);
|
|
2993
|
+
c.Persist = 2;
|
|
2994
|
+
try { return CredWriteW(ref c, 0); }
|
|
2995
|
+
finally { Marshal.FreeHGlobal(c.CredentialBlob); }
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
'@
|
|
2999
|
+
if (-not [CredWriter]::Write($target, $password)) { exit 1 }
|
|
3000
|
+
`;
|
|
3001
|
+
var WIN32_CRED_DELETE_SCRIPT = `
|
|
3002
|
+
Add-Type -TypeDefinition @'
|
|
3003
|
+
using System;
|
|
3004
|
+
using System.Runtime.InteropServices;
|
|
3005
|
+
public class CredDeleter {
|
|
3006
|
+
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
|
3007
|
+
private static extern bool CredDeleteW(string target, int type, int flags);
|
|
3008
|
+
public static bool Delete(string target) { return CredDeleteW(target, 1, 0); }
|
|
3009
|
+
}
|
|
3010
|
+
'@
|
|
3011
|
+
if (-not [CredDeleter]::Delete($target)) { exit 1 }
|
|
3012
|
+
`;
|
|
3013
|
+
function winTarget(service, account) {
|
|
3014
|
+
return `${service}/${account}`;
|
|
3015
|
+
}
|
|
3016
|
+
var windowsKeychain = {
|
|
3017
|
+
async getPassword(service, account) {
|
|
3018
|
+
try {
|
|
3019
|
+
const params = psParam("target", winTarget(service, account));
|
|
3020
|
+
const { stdout } = await execFileAsync(
|
|
3021
|
+
"powershell.exe",
|
|
3022
|
+
["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_READ_SCRIPT],
|
|
3023
|
+
{ timeout: 15e3, windowsHide: true }
|
|
3024
|
+
);
|
|
3025
|
+
return stdout;
|
|
3026
|
+
} catch {
|
|
3027
|
+
return null;
|
|
3028
|
+
}
|
|
3029
|
+
},
|
|
3030
|
+
async setPassword(service, account, password) {
|
|
3031
|
+
const params = psParam("target", winTarget(service, account)) + psParam("password", password);
|
|
3032
|
+
await execFileAsync(
|
|
3033
|
+
"powershell.exe",
|
|
3034
|
+
["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_WRITE_SCRIPT],
|
|
3035
|
+
{ timeout: 15e3, windowsHide: true }
|
|
3036
|
+
);
|
|
3037
|
+
},
|
|
3038
|
+
async deletePassword(service, account) {
|
|
3039
|
+
try {
|
|
3040
|
+
const params = psParam("target", winTarget(service, account));
|
|
3041
|
+
await execFileAsync(
|
|
3042
|
+
"powershell.exe",
|
|
3043
|
+
["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_DELETE_SCRIPT],
|
|
3044
|
+
{ timeout: 15e3, windowsHide: true }
|
|
3045
|
+
);
|
|
3046
|
+
return true;
|
|
3047
|
+
} catch {
|
|
3048
|
+
return false;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
};
|
|
3052
|
+
var macKeychain = {
|
|
3053
|
+
async getPassword(service, account) {
|
|
3054
|
+
try {
|
|
3055
|
+
const { stdout } = await execFileAsync(
|
|
3056
|
+
"security",
|
|
3057
|
+
["find-generic-password", "-s", service, "-a", account, "-w"],
|
|
3058
|
+
{ timeout: 15e3 }
|
|
3059
|
+
);
|
|
3060
|
+
return stdout.trimEnd();
|
|
3061
|
+
} catch {
|
|
3062
|
+
return null;
|
|
3063
|
+
}
|
|
3064
|
+
},
|
|
3065
|
+
async setPassword(service, account, password) {
|
|
3066
|
+
await execFileAsync(
|
|
3067
|
+
"security",
|
|
3068
|
+
["add-generic-password", "-U", "-s", service, "-a", account, "-w", password],
|
|
3069
|
+
{ timeout: 15e3 }
|
|
3070
|
+
);
|
|
3071
|
+
},
|
|
3072
|
+
async deletePassword(service, account) {
|
|
3073
|
+
try {
|
|
3074
|
+
await execFileAsync(
|
|
3075
|
+
"security",
|
|
3076
|
+
["delete-generic-password", "-s", service, "-a", account],
|
|
3077
|
+
{ timeout: 15e3 }
|
|
3078
|
+
);
|
|
3079
|
+
return true;
|
|
3080
|
+
} catch {
|
|
3081
|
+
return false;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
};
|
|
3085
|
+
var linuxKeychain = {
|
|
3086
|
+
async getPassword(service, account) {
|
|
3087
|
+
try {
|
|
3088
|
+
const { stdout } = await execFileAsync(
|
|
3089
|
+
"secret-tool",
|
|
3090
|
+
["lookup", "service", service, "account", account],
|
|
3091
|
+
{ timeout: 15e3 }
|
|
3092
|
+
);
|
|
3093
|
+
return stdout.trimEnd();
|
|
3094
|
+
} catch {
|
|
3095
|
+
return null;
|
|
3096
|
+
}
|
|
3097
|
+
},
|
|
3098
|
+
// secret-tool reads password from stdin (avoids exposing it in process args)
|
|
3099
|
+
async setPassword(service, account, password) {
|
|
3100
|
+
const child = spawn(
|
|
3101
|
+
"secret-tool",
|
|
3102
|
+
["store", "--label", `${service}/${account}`, "service", service, "account", account],
|
|
3103
|
+
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
3104
|
+
);
|
|
3105
|
+
child.stdin.write(password);
|
|
3106
|
+
child.stdin.end();
|
|
3107
|
+
await new Promise((resolve, reject) => {
|
|
3108
|
+
child.on(
|
|
3109
|
+
"close",
|
|
3110
|
+
(code) => code === 0 ? resolve() : reject(new Error("secret-tool store failed"))
|
|
3111
|
+
);
|
|
3112
|
+
child.on("error", reject);
|
|
3113
|
+
});
|
|
3114
|
+
},
|
|
3115
|
+
async deletePassword(service, account) {
|
|
3116
|
+
try {
|
|
3117
|
+
await execFileAsync(
|
|
3118
|
+
"secret-tool",
|
|
3119
|
+
["clear", "service", service, "account", account],
|
|
3120
|
+
{ timeout: 15e3 }
|
|
3121
|
+
);
|
|
3122
|
+
return true;
|
|
3123
|
+
} catch {
|
|
3124
|
+
return false;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
};
|
|
3128
|
+
function getBackend() {
|
|
3129
|
+
switch (process.platform) {
|
|
3130
|
+
case "win32":
|
|
3131
|
+
return windowsKeychain;
|
|
3132
|
+
case "darwin":
|
|
3133
|
+
return macKeychain;
|
|
3134
|
+
default:
|
|
3135
|
+
return linuxKeychain;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
var backend = getBackend();
|
|
3139
|
+
async function getPassword(service, account) {
|
|
3140
|
+
return backend.getPassword(service, account);
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
// src/secrets/index.ts
|
|
2048
3144
|
var SecretsLoader = class {
|
|
2049
3145
|
// Same service name as CLI uses
|
|
2050
3146
|
SERVICE_NAME = "sinch-functions-cli";
|
|
@@ -2058,24 +3154,12 @@ var SecretsLoader = class {
|
|
|
2058
3154
|
return false;
|
|
2059
3155
|
}
|
|
2060
3156
|
try {
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
keytar = await import("keytar");
|
|
2064
|
-
} catch (error) {
|
|
2065
|
-
if (error.code === "MODULE_NOT_FOUND" || error.code === "ERR_MODULE_NOT_FOUND") {
|
|
2066
|
-
console.debug("[Secrets] Keytar not available - secrets not loaded");
|
|
2067
|
-
return false;
|
|
2068
|
-
} else {
|
|
2069
|
-
console.error("[Secrets] Error loading keytar:", error.message);
|
|
2070
|
-
}
|
|
2071
|
-
return false;
|
|
2072
|
-
}
|
|
2073
|
-
const envPath = path4.join(process.cwd(), ".env");
|
|
2074
|
-
if (!fs3.existsSync(envPath)) {
|
|
3157
|
+
const envPath = path5.join(process.cwd(), ".env");
|
|
3158
|
+
if (!fs4.existsSync(envPath)) {
|
|
2075
3159
|
console.debug("[Secrets] No .env file found, skipping keychain load");
|
|
2076
3160
|
return false;
|
|
2077
3161
|
}
|
|
2078
|
-
const envContent =
|
|
3162
|
+
const envContent = fs4.readFileSync(envPath, "utf8");
|
|
2079
3163
|
const envLines = envContent.replace(/\r\n/g, "\n").split("\n");
|
|
2080
3164
|
const secretsToLoad = [];
|
|
2081
3165
|
envLines.forEach((line) => {
|
|
@@ -2097,7 +3181,7 @@ var SecretsLoader = class {
|
|
|
2097
3181
|
}
|
|
2098
3182
|
let secretsLoaded = 0;
|
|
2099
3183
|
if (secretsToLoad.includes("PROJECT_ID_API_SECRET")) {
|
|
2100
|
-
const apiSecret = await
|
|
3184
|
+
const apiSecret = await getPassword(this.SERVICE_NAME, `${this.username}-keySecret`);
|
|
2101
3185
|
if (apiSecret) {
|
|
2102
3186
|
process.env.PROJECT_ID_API_SECRET = apiSecret;
|
|
2103
3187
|
console.log("\u2705 Loaded PROJECT_ID_API_SECRET from secure storage");
|
|
@@ -2107,7 +3191,7 @@ var SecretsLoader = class {
|
|
|
2107
3191
|
if (secretsToLoad.includes("VOICE_APPLICATION_SECRET")) {
|
|
2108
3192
|
const applicationKey = process.env.VOICE_APPLICATION_KEY || this.getApplicationKeyFromConfig();
|
|
2109
3193
|
if (applicationKey) {
|
|
2110
|
-
const appSecret = await
|
|
3194
|
+
const appSecret = await getPassword(this.SERVICE_NAME, applicationKey);
|
|
2111
3195
|
if (appSecret) {
|
|
2112
3196
|
process.env.VOICE_APPLICATION_SECRET = appSecret;
|
|
2113
3197
|
console.log("\u2705 Loaded VOICE_APPLICATION_SECRET from secure storage");
|
|
@@ -2121,7 +3205,7 @@ var SecretsLoader = class {
|
|
|
2121
3205
|
continue;
|
|
2122
3206
|
}
|
|
2123
3207
|
if (functionName) {
|
|
2124
|
-
const value = await
|
|
3208
|
+
const value = await getPassword(
|
|
2125
3209
|
this.SERVICE_NAME,
|
|
2126
3210
|
`${functionName}-${secretName}`
|
|
2127
3211
|
);
|
|
@@ -2149,9 +3233,9 @@ var SecretsLoader = class {
|
|
|
2149
3233
|
*/
|
|
2150
3234
|
getApplicationKeyFromConfig() {
|
|
2151
3235
|
try {
|
|
2152
|
-
const sinchJsonPath =
|
|
2153
|
-
if (
|
|
2154
|
-
const sinchConfig = JSON.parse(
|
|
3236
|
+
const sinchJsonPath = path5.join(process.cwd(), "sinch.json");
|
|
3237
|
+
if (fs4.existsSync(sinchJsonPath)) {
|
|
3238
|
+
const sinchConfig = JSON.parse(fs4.readFileSync(sinchJsonPath, "utf8"));
|
|
2155
3239
|
return sinchConfig.voiceAppId || sinchConfig.applicationKey || null;
|
|
2156
3240
|
}
|
|
2157
3241
|
} catch (error) {
|
|
@@ -2164,9 +3248,9 @@ var SecretsLoader = class {
|
|
|
2164
3248
|
*/
|
|
2165
3249
|
getFunctionNameFromConfig() {
|
|
2166
3250
|
try {
|
|
2167
|
-
const sinchJsonPath =
|
|
2168
|
-
if (
|
|
2169
|
-
const sinchConfig = JSON.parse(
|
|
3251
|
+
const sinchJsonPath = path5.join(process.cwd(), "sinch.json");
|
|
3252
|
+
if (fs4.existsSync(sinchJsonPath)) {
|
|
3253
|
+
const sinchConfig = JSON.parse(fs4.readFileSync(sinchJsonPath, "utf8"));
|
|
2170
3254
|
return sinchConfig.name || null;
|
|
2171
3255
|
}
|
|
2172
3256
|
} catch (error) {
|
|
@@ -2180,14 +3264,13 @@ var SecretsLoader = class {
|
|
|
2180
3264
|
async loadCustomSecrets(secretNames = []) {
|
|
2181
3265
|
const secrets = {};
|
|
2182
3266
|
try {
|
|
2183
|
-
const keytar = await import("keytar");
|
|
2184
3267
|
const functionName = this.getFunctionNameFromConfig();
|
|
2185
3268
|
if (!functionName) {
|
|
2186
3269
|
console.debug("[Secrets] Could not determine function name for custom secrets");
|
|
2187
3270
|
return secrets;
|
|
2188
3271
|
}
|
|
2189
3272
|
for (const secretName of secretNames) {
|
|
2190
|
-
const value = await
|
|
3273
|
+
const value = await getPassword(this.SERVICE_NAME, `${functionName}-${secretName}`);
|
|
2191
3274
|
if (value) {
|
|
2192
3275
|
secrets[secretName] = value;
|
|
2193
3276
|
process.env[secretName] = value;
|
|
@@ -2199,58 +3282,88 @@ var SecretsLoader = class {
|
|
|
2199
3282
|
return secrets;
|
|
2200
3283
|
}
|
|
2201
3284
|
/**
|
|
2202
|
-
* Check if
|
|
3285
|
+
* Check if the native keychain is available.
|
|
3286
|
+
* Always true — no native npm deps required. The underlying OS tool
|
|
3287
|
+
* (secret-tool, security, CredManager) may still be absent, in which
|
|
3288
|
+
* case getPassword() silently returns null per credential.
|
|
2203
3289
|
*/
|
|
2204
3290
|
async isAvailable() {
|
|
2205
|
-
|
|
2206
|
-
await import("keytar");
|
|
2207
|
-
return true;
|
|
2208
|
-
} catch {
|
|
2209
|
-
return false;
|
|
2210
|
-
}
|
|
3291
|
+
return true;
|
|
2211
3292
|
}
|
|
2212
3293
|
};
|
|
2213
3294
|
var secretsLoader = new SecretsLoader();
|
|
2214
3295
|
export {
|
|
2215
3296
|
AceSvamlBuilder,
|
|
2216
|
-
|
|
3297
|
+
AgentProvider,
|
|
3298
|
+
ConversationController,
|
|
3299
|
+
ConversationMessage,
|
|
3300
|
+
ConversationMessageBuilder,
|
|
3301
|
+
ElevenLabsAutoConfigurator,
|
|
3302
|
+
ElevenLabsClient,
|
|
3303
|
+
ElevenLabsController,
|
|
3304
|
+
ElevenLabsState,
|
|
2217
3305
|
IceSvamlBuilder,
|
|
2218
3306
|
LocalCache,
|
|
3307
|
+
LocalStorage,
|
|
2219
3308
|
MenuBuilder,
|
|
2220
3309
|
MenuTemplates,
|
|
2221
3310
|
NOTIFICATION_EVENTS,
|
|
2222
3311
|
PieSvamlBuilder,
|
|
2223
3312
|
SecretsLoader,
|
|
2224
|
-
TemplateRender,
|
|
2225
3313
|
TunnelClient,
|
|
2226
3314
|
UniversalConfig,
|
|
2227
3315
|
VOICE_CALLBACKS,
|
|
2228
|
-
|
|
3316
|
+
VoiceErrorHelper,
|
|
2229
3317
|
buildBaseContext,
|
|
2230
3318
|
toCamelCase as convertToCamelCase,
|
|
2231
3319
|
createAceBuilder,
|
|
2232
3320
|
createApp,
|
|
2233
3321
|
createCacheClient,
|
|
3322
|
+
createChannelMessage,
|
|
2234
3323
|
createConfig,
|
|
3324
|
+
createElevenLabsClient,
|
|
2235
3325
|
createErrorResponse,
|
|
2236
3326
|
createIceBuilder,
|
|
2237
3327
|
createLenientJsonParser as createJsonParser,
|
|
2238
3328
|
createJsonResponse,
|
|
2239
3329
|
createLenientJsonParser,
|
|
2240
3330
|
createMenu,
|
|
3331
|
+
createMessageBuilder,
|
|
3332
|
+
createMessageHelper,
|
|
3333
|
+
createMessenger,
|
|
2241
3334
|
createPieBuilder,
|
|
2242
3335
|
createResponse,
|
|
2243
3336
|
createSimpleMenu,
|
|
3337
|
+
createSms,
|
|
3338
|
+
createStorageClient,
|
|
2244
3339
|
createUniversalConfig,
|
|
3340
|
+
createWhatsApp,
|
|
3341
|
+
extractCallerNumber,
|
|
2245
3342
|
extractFunctionName,
|
|
2246
3343
|
formatCustomResponse,
|
|
3344
|
+
formatDuration,
|
|
3345
|
+
formatPhoneNumber,
|
|
2247
3346
|
formatSvamlResponse,
|
|
2248
3347
|
generateRequestId,
|
|
3348
|
+
getChannel,
|
|
3349
|
+
getContactId,
|
|
3350
|
+
getConversationId,
|
|
3351
|
+
getFaviconSvg,
|
|
3352
|
+
getIdentity,
|
|
3353
|
+
getLandingPageHtml,
|
|
3354
|
+
getLocation,
|
|
3355
|
+
getMedia,
|
|
3356
|
+
getPostbackData,
|
|
2249
3357
|
getSinchClients,
|
|
3358
|
+
getText,
|
|
3359
|
+
getTo,
|
|
2250
3360
|
getTunnelClient,
|
|
2251
3361
|
handleCustomEndpoint,
|
|
2252
3362
|
handleVoiceCallback,
|
|
3363
|
+
isMediaMessage,
|
|
2253
3364
|
isNotificationEvent,
|
|
3365
|
+
isPostback,
|
|
3366
|
+
isTextMessage,
|
|
2254
3367
|
isVoiceCallback,
|
|
2255
3368
|
parseJson,
|
|
2256
3369
|
parseJson as parseJsonLenient,
|
|
@@ -2259,9 +3372,12 @@ export {
|
|
|
2259
3372
|
setupJsonParsing as setupJsonMiddleware,
|
|
2260
3373
|
setupJsonParsing,
|
|
2261
3374
|
setupRequestHandler,
|
|
3375
|
+
textReply,
|
|
2262
3376
|
toCamelCase,
|
|
3377
|
+
toLocalPhoneNumber,
|
|
2263
3378
|
transformKeys,
|
|
2264
3379
|
transformKeys as transformKeysToCamelCase,
|
|
3380
|
+
tryAutoConfigureAsync,
|
|
2265
3381
|
validateVoiceRequest
|
|
2266
3382
|
};
|
|
2267
3383
|
//# sourceMappingURL=index.js.map
|