@openclaw/voice-call 2026.7.2-beta.5 → 2026.8.1-beta.2
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/doctor-contract-api.js +10 -8
- package/dist/{guarded-json-api-Dxhe25Zi.js → guarded-json-api-DEDwgf4J.js} +43 -53
- package/dist/index.js +328 -637
- package/dist/{mock-YsvBTX-7.js → mock-BCq0q36l.js} +2 -3
- package/dist/{plivo-B-KvPVji.js → plivo-DbI17uAc.js} +45 -8
- package/dist/{realtime-handler-hIVPEZxN.js → realtime-handler-uXR0QgVa.js} +366 -109
- package/dist/{response-generator-z6M9229t.js → response-generator-D-hkzo5Y.js} +13 -5
- package/dist/{runtime-entry-CQtitvRd.js → runtime-entry-F13eJ4pp.js} +279 -172
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +17 -24
- package/dist/{store-path-nYL-yM0S.js → store-path-51_oC_xn.js} +70 -13
- package/dist/{telnyx-BG56lL7z.js → telnyx-sIS_WgnY.js} +3 -2
- package/dist/{twilio-B3FQ_P9x.js → twilio-CbeVrVAl.js} +20 -26
- package/dist/{webhook-replay-aaP79dI8.js → webhook-replay-FQrEGVSM.js} +20 -6
- package/openclaw.plugin.json +49 -4
- package/package.json +4 -4
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { a as CALL_RECORD_EVENT_META_MAX_ENTRIES, c as buildVoiceCallLegacyJsonlEventKey, f as parseVoiceCallRecordLine, h as resolveVoiceCallLegacyCallLogPath, i as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, m as prepareVoiceCallRecordForStorage, n as CALL_RECORD_CHUNK_MAX_ENTRIES, o as MAX_CALL_RECORD_EVENTS, r as CALL_RECORD_EVENTS_NAMESPACE, s as RAW_CALL_RECORD_CHUNK_BYTES, t as resolveDefaultVoiceCallStoreDir } from "./store-path-
|
|
1
|
+
import { a as CALL_RECORD_EVENT_META_MAX_ENTRIES, c as buildVoiceCallLegacyJsonlEventKey, f as parseVoiceCallRecordLine, h as resolveVoiceCallLegacyCallLogPath, i as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, m as prepareVoiceCallRecordForStorage, n as CALL_RECORD_CHUNK_MAX_ENTRIES, o as MAX_CALL_RECORD_EVENTS, r as CALL_RECORD_EVENTS_NAMESPACE, s as RAW_CALL_RECORD_CHUNK_BYTES, t as resolveDefaultVoiceCallStoreDir } from "./store-path-51_oC_xn.js";
|
|
2
2
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
3
|
+
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import fs from "node:fs/promises";
|
|
6
|
-
import { archiveLegacyStateSource
|
|
7
|
+
import { archiveLegacyStateSource } from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
|
7
8
|
//#region extensions/voice-call/doctor-contract-api.ts
|
|
8
9
|
/** Resolve home from doctor env with OS fallback. */
|
|
9
10
|
function resolveHome(env) {
|
|
@@ -26,21 +27,18 @@ function getVoiceCallConfigStore(config) {
|
|
|
26
27
|
}
|
|
27
28
|
return "";
|
|
28
29
|
}
|
|
29
|
-
function asRecord(value) {
|
|
30
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
31
|
-
}
|
|
32
30
|
/** Return Voice Call agents whose templated core session stores need migration. */
|
|
33
31
|
function resolveSessionStoreAgentIds(params) {
|
|
34
32
|
const agentIds = /* @__PURE__ */ new Set();
|
|
35
33
|
for (const pluginId of ["voice-call", "@openclaw/voice-call"]) {
|
|
36
34
|
const entry = params.cfg.plugins?.entries?.[pluginId];
|
|
37
35
|
if (!entry) continue;
|
|
38
|
-
const config = entry.config === void 0 ? {} :
|
|
36
|
+
const config = entry.config === void 0 ? {} : asOptionalRecord(entry.config);
|
|
39
37
|
if (!config) continue;
|
|
40
38
|
agentIds.add(normalizeAgentId(typeof config.agentId === "string" ? config.agentId : void 0));
|
|
41
|
-
const numbers =
|
|
39
|
+
const numbers = asOptionalRecord(config.numbers);
|
|
42
40
|
for (const route of Object.values(numbers ?? {})) {
|
|
43
|
-
const agentId =
|
|
41
|
+
const agentId = asOptionalRecord(route)?.agentId;
|
|
44
42
|
if (typeof agentId === "string") agentIds.add(normalizeAgentId(agentId));
|
|
45
43
|
}
|
|
46
44
|
}
|
|
@@ -62,6 +60,8 @@ function describeVoiceCallSchemaMigration(migration) {
|
|
|
62
60
|
switch (migration.kind) {
|
|
63
61
|
case "agent-databases-composite-primary-key": return "agent database registry primary key -> agent_id,path";
|
|
64
62
|
case "audit-events-v2": return "audit event ledger -> versioned message lifecycle schema";
|
|
63
|
+
case "commitments-retirement-v7": return "retired commitments storage -> removed table and indexes";
|
|
64
|
+
case "worker-placement-execution-mode-v8": return "cloud worker placements -> execution-mode claims";
|
|
65
65
|
case "operator-approvals-system-agent": return "operator approvals -> OpenClaw system changes";
|
|
66
66
|
case "session-watch-cursor-provenance-v4": return "session watch cursors -> provenance column";
|
|
67
67
|
case "strict-tables-v3": return "tables -> SQLite STRICT typing";
|
|
@@ -185,12 +185,14 @@ const stateMigrations = [{
|
|
|
185
185
|
id: "voice-call-calls-jsonl-to-plugin-state",
|
|
186
186
|
label: "Voice Call call log",
|
|
187
187
|
async detectLegacyState(params) {
|
|
188
|
+
const { detectOpenClawStateDatabaseSchemaMigrations } = await import("openclaw/plugin-sdk/doctor-repair-runtime");
|
|
188
189
|
const { entries } = await readLegacyCallRecords(resolveVoiceCallLegacyCallLogPath(resolveVoiceCallStorePath(params)));
|
|
189
190
|
const schemaMigrations = detectOpenClawStateDatabaseSchemaMigrations({ env: resolveVoiceCallStateDatabaseEnv(params) });
|
|
190
191
|
if (entries.length === 0 && schemaMigrations.length === 0) return null;
|
|
191
192
|
return { preview: [...schemaMigrations.map((migration) => `- Voice Call SQLite schema: ${describeVoiceCallSchemaMigration(migration)}`), ...entries.length > 0 ? [`- Voice Call call log: ${entries.length} ${entries.length === 1 ? "record" : "records"} -> plugin state (${CALL_RECORD_EVENTS_NAMESPACE})`] : []] };
|
|
192
193
|
},
|
|
193
194
|
async migrateLegacyState(params) {
|
|
195
|
+
const { detectOpenClawStateDatabaseSchemaMigrations, repairOpenClawStateDatabaseSchema } = await import("openclaw/plugin-sdk/doctor-repair-runtime");
|
|
194
196
|
const changes = [];
|
|
195
197
|
const warnings = [];
|
|
196
198
|
const filePath = resolveVoiceCallLegacyCallLogPath(resolveVoiceCallStorePath(params));
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { n as normalizeProxyIp, s as getHeader } from "./runtime-entry-
|
|
4
|
-
import { n as
|
|
3
|
+
import { n as normalizeProxyIp, s as getHeader } from "./runtime-entry-F13eJ4pp.js";
|
|
4
|
+
import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-FQrEGVSM.js";
|
|
5
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
6
6
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
7
7
|
import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
8
|
import crypto from "node:crypto";
|
|
9
|
-
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
9
|
+
import { redactSensitiveText, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
10
|
+
import { isIP } from "node:net";
|
|
10
11
|
import { readResponseTextPrefix, readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
|
11
12
|
//#region extensions/voice-call/src/webhook-security.ts
|
|
12
13
|
const twilioReplayCache = createWebhookReplayCache();
|
|
@@ -57,7 +58,17 @@ function extractHostname(hostHeader) {
|
|
|
57
58
|
if (hostHeader.startsWith("[")) {
|
|
58
59
|
const endBracket = hostHeader.indexOf("]");
|
|
59
60
|
if (endBracket === -1) return null;
|
|
60
|
-
|
|
61
|
+
const suffix = hostHeader.slice(endBracket + 1);
|
|
62
|
+
if (suffix && !/^:\d+$/u.test(suffix)) return null;
|
|
63
|
+
const hostname = hostHeader.slice(1, endBracket);
|
|
64
|
+
if (isIP(hostname) !== 6) return null;
|
|
65
|
+
try {
|
|
66
|
+
const parsedHostname = new URL(`https://[${hostname}]/`).hostname;
|
|
67
|
+
if (!parsedHostname.startsWith("[") || !parsedHostname.endsWith("]")) return null;
|
|
68
|
+
return normalizeLowercaseStringOrEmpty(parsedHostname.slice(1, -1));
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
61
72
|
}
|
|
62
73
|
if (hostHeader.includes("@")) return null;
|
|
63
74
|
const hostname = hostHeader.split(":").at(0);
|
|
@@ -78,6 +89,9 @@ function normalizeAllowedHosts(allowedHosts) {
|
|
|
78
89
|
}
|
|
79
90
|
return normalized.size > 0 ? normalized : null;
|
|
80
91
|
}
|
|
92
|
+
function formatHostnameForUrl(hostname) {
|
|
93
|
+
return isIP(hostname) === 6 ? `[${hostname}]` : hostname;
|
|
94
|
+
}
|
|
81
95
|
/**
|
|
82
96
|
* Reconstruct the public webhook URL from request headers.
|
|
83
97
|
*
|
|
@@ -147,15 +161,13 @@ function reconstructWebhookUrl(ctx, options) {
|
|
|
147
161
|
const parsed = new URL(ctx.url);
|
|
148
162
|
path = parsed.pathname + parsed.search;
|
|
149
163
|
} catch {}
|
|
150
|
-
return `${proto}://${host}${path}`;
|
|
164
|
+
return `${proto}://${formatHostnameForUrl(host)}${path}`;
|
|
151
165
|
}
|
|
152
166
|
function buildTwilioVerificationUrl(ctx, publicUrl, urlOptions) {
|
|
153
167
|
if (!publicUrl) return reconstructWebhookUrl(ctx, urlOptions);
|
|
154
168
|
try {
|
|
155
169
|
const base = new URL(publicUrl);
|
|
156
|
-
|
|
157
|
-
base.pathname = requestUrl.pathname;
|
|
158
|
-
base.search = requestUrl.search;
|
|
170
|
+
base.search = new URL(ctx.url).search;
|
|
159
171
|
return base.toString();
|
|
160
172
|
} catch {
|
|
161
173
|
return publicUrl;
|
|
@@ -244,8 +256,7 @@ function verifyTelnyxWebhook(ctx, publicKey, options) {
|
|
|
244
256
|
return {
|
|
245
257
|
ok: true,
|
|
246
258
|
reason: "verification skipped (dev mode)",
|
|
247
|
-
|
|
248
|
-
verifiedRequestKey: replayKey
|
|
259
|
+
...reserveWebhookReplay(telnyxReplayCache, replayKey)
|
|
249
260
|
};
|
|
250
261
|
}
|
|
251
262
|
if (!publicKey) return {
|
|
@@ -258,8 +269,8 @@ function verifyTelnyxWebhook(ctx, publicKey, options) {
|
|
|
258
269
|
ok: false,
|
|
259
270
|
reason: "Missing signature or timestamp header"
|
|
260
271
|
};
|
|
261
|
-
const eventTimeSec = Number
|
|
262
|
-
if (!Number.
|
|
272
|
+
const eventTimeSec = /^(?:0|[1-9]\d*)$/.test(timestamp) ? Number(timestamp) : void 0;
|
|
273
|
+
if (eventTimeSec === void 0 || !Number.isSafeInteger(eventTimeSec)) return {
|
|
263
274
|
ok: false,
|
|
264
275
|
reason: "Invalid timestamp header"
|
|
265
276
|
};
|
|
@@ -281,8 +292,7 @@ function verifyTelnyxWebhook(ctx, publicKey, options) {
|
|
|
281
292
|
const replayKey = `telnyx:${sha256Hex(`${timestamp}\n${canonicalSignature}\n${ctx.rawBody}`)}`;
|
|
282
293
|
return {
|
|
283
294
|
ok: true,
|
|
284
|
-
|
|
285
|
-
verifiedRequestKey: replayKey
|
|
295
|
+
...reserveWebhookReplay(telnyxReplayCache, replayKey)
|
|
286
296
|
};
|
|
287
297
|
} catch (err) {
|
|
288
298
|
return {
|
|
@@ -300,8 +310,7 @@ function verifyTwilioWebhook(ctx, authToken, options) {
|
|
|
300
310
|
return {
|
|
301
311
|
ok: true,
|
|
302
312
|
reason: "verification skipped (dev mode)",
|
|
303
|
-
|
|
304
|
-
verifiedRequestKey: replayKey
|
|
313
|
+
...reserveWebhookReplay(twilioReplayCache, replayKey)
|
|
305
314
|
};
|
|
306
315
|
}
|
|
307
316
|
const signature = getHeader(ctx.headers, "x-twilio-signature");
|
|
@@ -327,8 +336,7 @@ function verifyTwilioWebhook(ctx, authToken, options) {
|
|
|
327
336
|
return {
|
|
328
337
|
ok: true,
|
|
329
338
|
verificationUrl,
|
|
330
|
-
|
|
331
|
-
verifiedRequestKey: replayKey
|
|
339
|
+
...reserveWebhookReplay(twilioReplayCache, replayKey)
|
|
332
340
|
};
|
|
333
341
|
}
|
|
334
342
|
const variants = /* @__PURE__ */ new Set();
|
|
@@ -351,8 +359,7 @@ function verifyTwilioWebhook(ctx, authToken, options) {
|
|
|
351
359
|
return {
|
|
352
360
|
ok: true,
|
|
353
361
|
verificationUrl: candidateUrl,
|
|
354
|
-
|
|
355
|
-
verifiedRequestKey: replayKey
|
|
362
|
+
...reserveWebhookReplay(twilioReplayCache, replayKey)
|
|
356
363
|
};
|
|
357
364
|
}
|
|
358
365
|
const isNgrokFreeTier = verificationUrl.includes(".ngrok-free.app") || verificationUrl.includes(".ngrok.io");
|
|
@@ -444,8 +451,7 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
444
451
|
return {
|
|
445
452
|
ok: true,
|
|
446
453
|
reason: "verification skipped (dev mode)",
|
|
447
|
-
|
|
448
|
-
verifiedRequestKey: replayKey
|
|
454
|
+
...reserveWebhookReplay(plivoReplayCache, replayKey)
|
|
449
455
|
};
|
|
450
456
|
}
|
|
451
457
|
const signatureV3 = getHeader(ctx.headers, "x-plivo-signature-v3");
|
|
@@ -495,13 +501,11 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
495
501
|
postParams,
|
|
496
502
|
nonce: nonceV3
|
|
497
503
|
});
|
|
498
|
-
const isReplay = markWebhookReplay(plivoReplayCache, replayKey);
|
|
499
504
|
return {
|
|
500
505
|
ok: true,
|
|
501
506
|
version: "v3",
|
|
502
507
|
verificationUrl,
|
|
503
|
-
|
|
504
|
-
verifiedRequestKey: replayKey
|
|
508
|
+
...reserveWebhookReplay(plivoReplayCache, replayKey)
|
|
505
509
|
};
|
|
506
510
|
}
|
|
507
511
|
if (signatureV2 && nonceV2) {
|
|
@@ -517,13 +521,11 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
517
521
|
reason: "Invalid Plivo V2 signature"
|
|
518
522
|
};
|
|
519
523
|
const replayKey = createPlivoV2ReplayKey(verificationUrl, nonceV2);
|
|
520
|
-
const isReplay = markWebhookReplay(plivoReplayCache, replayKey);
|
|
521
524
|
return {
|
|
522
525
|
ok: true,
|
|
523
526
|
version: "v2",
|
|
524
527
|
verificationUrl,
|
|
525
|
-
|
|
526
|
-
verifiedRequestKey: replayKey
|
|
528
|
+
...reserveWebhookReplay(plivoReplayCache, replayKey)
|
|
527
529
|
};
|
|
528
530
|
}
|
|
529
531
|
return {
|
|
@@ -543,26 +545,20 @@ async function cancelProviderResponseBody(response) {
|
|
|
543
545
|
function appendTruncatedSuffix(text) {
|
|
544
546
|
return `${text.trimEnd()}${TRUNCATED_SUFFIX}`;
|
|
545
547
|
}
|
|
546
|
-
async function
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
548
|
+
async function readVoiceCallProviderJsonResponse(response, malformedJsonMessage) {
|
|
549
|
+
const body = await readResponseWithLimit(response, PROVIDER_JSON_RESPONSE_MAX_BYTES, { onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`provider response body too large: ${size} bytes (limit: ${maxBytes} bytes)`) });
|
|
550
|
+
if (body.byteLength === 0) return;
|
|
551
|
+
try {
|
|
552
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
553
|
+
return JSON.parse(text);
|
|
554
|
+
} catch (cause) {
|
|
555
|
+
throw new Error(malformedJsonMessage, { cause });
|
|
550
556
|
}
|
|
551
|
-
const body = await readResponseWithLimit(params.response, params.maxBytes, { onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`provider response body too large: ${size} bytes (limit: ${maxBytes} bytes)`) });
|
|
552
|
-
return new TextDecoder().decode(body);
|
|
553
|
-
}
|
|
554
|
-
async function readProviderJsonResponseText(response) {
|
|
555
|
-
return await readProviderResponseTextWithLimit({
|
|
556
|
-
response,
|
|
557
|
-
maxBytes: PROVIDER_JSON_RESPONSE_MAX_BYTES
|
|
558
|
-
});
|
|
559
557
|
}
|
|
560
558
|
async function readProviderErrorResponseSnippet(response) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
truncateOnLimit: true
|
|
565
|
-
});
|
|
559
|
+
const prefix = await readResponseTextPrefix(response, PROVIDER_ERROR_RESPONSE_MAX_BYTES);
|
|
560
|
+
const text = redactSensitiveText(prefix.text, { mode: "tools" });
|
|
561
|
+
return prefix.truncated ? appendTruncatedSuffix(text) : text;
|
|
566
562
|
}
|
|
567
563
|
//#endregion
|
|
568
564
|
//#region extensions/voice-call/src/providers/shared/guarded-json-api.ts
|
|
@@ -589,16 +585,10 @@ async function guardedJsonApiRequest(params) {
|
|
|
589
585
|
const errorText = await readProviderErrorResponseSnippet(response);
|
|
590
586
|
throw new Error(`${params.errorPrefix}: ${response.status} ${errorText}`);
|
|
591
587
|
}
|
|
592
|
-
|
|
593
|
-
if (!text) return;
|
|
594
|
-
try {
|
|
595
|
-
return JSON.parse(text);
|
|
596
|
-
} catch {
|
|
597
|
-
throw new Error(`${params.errorPrefix}: malformed JSON response`);
|
|
598
|
-
}
|
|
588
|
+
return await readVoiceCallProviderJsonResponse(response, `${params.errorPrefix}: malformed JSON response`);
|
|
599
589
|
} finally {
|
|
600
590
|
await release();
|
|
601
591
|
}
|
|
602
592
|
}
|
|
603
593
|
//#endregion
|
|
604
|
-
export { reconstructWebhookUrl as a, verifyTwilioWebhook as c,
|
|
594
|
+
export { reconstructWebhookUrl as a, verifyTwilioWebhook as c, readVoiceCallProviderJsonResponse as i, cancelProviderResponseBody as n, verifyPlivoWebhook as o, readProviderErrorResponseSnippet as r, verifyTelnyxWebhook as s, guardedJsonApiRequest as t };
|