@absolutejs/voice 0.0.22-beta.77 → 0.0.22-beta.79
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.d.ts +4 -0
- package/dist/index.js +623 -0
- package/dist/telephony/plivo.d.ts +154 -0
- package/dist/telephony/telnyx.d.ts +139 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -82,8 +82,12 @@ export type { VoiceSQLiteRuntimeStorage, VoiceSQLiteStoreOptions } from './sqlit
|
|
|
82
82
|
export type { StoredVoiceIntegrationEvent, StoredVoiceExternalObjectMap, StoredVoiceOpsTask, VoiceExternalObjectMap, VoiceExternalObjectMapStore, VoiceOpsTaskAgeBucket, VoiceOpsTaskAnalyticsOptions, VoiceOpsTaskAnalyticsSummary, VoiceOpsTaskAssignmentRule, VoiceOpsTaskAssignmentRuleCondition, VoiceOpsTaskAssignmentRules, VoiceOpsTaskAssigneeAnalytics, VoiceOpsDispositionTaskPolicies, VoiceOpsSLABreachPolicy, VoiceIntegrationDeliveryStatus, VoiceIntegrationEvent, VoiceIntegrationEventStore, VoiceIntegrationSinkDelivery, VoiceIntegrationEventType, VoiceIntegrationWebhookConfig, VoiceOpsTask, VoiceOpsTaskHistoryEntry, VoiceOpsTaskKind, VoiceOpsTaskPolicy, VoiceOpsTaskPriority, VoiceOpsTaskStatus, VoiceOpsTaskStore, VoiceOpsTaskSummary, VoiceOpsTaskWorkerAnalytics } from './ops';
|
|
83
83
|
export { createTwilioMediaStreamBridge, createTwilioVoiceRoutes, createTwilioVoiceResponse, decodeTwilioMulawBase64, encodeTwilioMulawBase64, transcodePCMToTwilioOutboundPayload, transcodeTwilioInboundPayloadToPCM16 } from './telephony/twilio';
|
|
84
84
|
export { evaluateVoiceTelephonyContract } from './telephony/contract';
|
|
85
|
+
export { createTelnyxVoiceResponse, createTelnyxVoiceRoutes, verifyVoiceTelnyxWebhookSignature } from './telephony/telnyx';
|
|
86
|
+
export { createPlivoVoiceResponse, createPlivoVoiceRoutes, signVoicePlivoWebhook, verifyVoicePlivoWebhookSignature } from './telephony/plivo';
|
|
85
87
|
export type { TwilioInboundMessage, TwilioMediaStreamBridge, TwilioMediaStreamBridgeOptions, TwilioMediaStreamSocket, TwilioOutboundClearMessage, TwilioOutboundMarkMessage, TwilioOutboundMediaMessage, TwilioOutboundMessage, TwilioVoiceRouteParameters, TwilioVoiceResponseOptions, TwilioVoiceSmokeCheck, TwilioVoiceSmokeOptions, TwilioVoiceSmokeReport, TwilioVoiceSetupOptions, TwilioVoiceSetupStatus, TwilioVoiceRoutesOptions } from './telephony/twilio';
|
|
86
88
|
export type { VoiceTelephonyContractIssue, VoiceTelephonyContractOptions, VoiceTelephonyContractReport, VoiceTelephonyContractRequirement, VoiceTelephonyProvider, VoiceTelephonySetupStatus, VoiceTelephonySmokeCheck, VoiceTelephonySmokeReport } from './telephony/contract';
|
|
89
|
+
export type { TelnyxVoiceResponseOptions, TelnyxVoiceRoutesOptions, TelnyxVoiceSetupOptions, TelnyxVoiceSetupStatus, TelnyxVoiceSmokeCheck, TelnyxVoiceSmokeOptions, TelnyxVoiceSmokeReport } from './telephony/telnyx';
|
|
90
|
+
export type { PlivoVoiceResponseOptions, PlivoVoiceRoutesOptions, PlivoVoiceSetupOptions, PlivoVoiceSetupStatus, PlivoVoiceSmokeCheck, PlivoVoiceSmokeOptions, PlivoVoiceSmokeReport } from './telephony/plivo';
|
|
87
91
|
export { shapeTelephonyAssistantText } from './telephony/response';
|
|
88
92
|
export type { TelephonyResponseShapeMode, TelephonyResponseShapeOptions } from './telephony/response';
|
|
89
93
|
export * from './types';
|
package/dist/index.js
CHANGED
|
@@ -15983,6 +15983,622 @@ var evaluateVoiceTelephonyContract = (input) => {
|
|
|
15983
15983
|
smoke: input.smoke
|
|
15984
15984
|
};
|
|
15985
15985
|
};
|
|
15986
|
+
// src/telephony/telnyx.ts
|
|
15987
|
+
import { Buffer as Buffer4 } from "buffer";
|
|
15988
|
+
import { Elysia as Elysia19 } from "elysia";
|
|
15989
|
+
var escapeXml3 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
15990
|
+
var escapeHtml18 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
15991
|
+
var joinUrlPath2 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
15992
|
+
var resolveRequestOrigin2 = (request) => {
|
|
15993
|
+
const url = new URL(request.url);
|
|
15994
|
+
const forwardedHost = request.headers.get("x-forwarded-host");
|
|
15995
|
+
const forwardedProto = request.headers.get("x-forwarded-proto");
|
|
15996
|
+
const host = forwardedHost ?? request.headers.get("host") ?? url.host;
|
|
15997
|
+
const protocol = forwardedProto ?? url.protocol.replace(":", "");
|
|
15998
|
+
return `${protocol}://${host}`;
|
|
15999
|
+
};
|
|
16000
|
+
var extractTelnyxStreamUrl = (texml) => texml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&", "&");
|
|
16001
|
+
var createSmokeCheck2 = (name, status, message, details) => ({
|
|
16002
|
+
details,
|
|
16003
|
+
message,
|
|
16004
|
+
name,
|
|
16005
|
+
status
|
|
16006
|
+
});
|
|
16007
|
+
var resolveTelnyxStreamUrl = async (options, input) => {
|
|
16008
|
+
if (typeof options.texml?.streamUrl === "function") {
|
|
16009
|
+
return options.texml.streamUrl(input);
|
|
16010
|
+
}
|
|
16011
|
+
if (typeof options.texml?.streamUrl === "string") {
|
|
16012
|
+
return options.texml.streamUrl;
|
|
16013
|
+
}
|
|
16014
|
+
const origin = resolveRequestOrigin2(input.request);
|
|
16015
|
+
const wsOrigin = origin.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
|
|
16016
|
+
return `${wsOrigin}${input.streamPath}`;
|
|
16017
|
+
};
|
|
16018
|
+
var createTelnyxVoiceResponse = (options) => {
|
|
16019
|
+
const attributes = [
|
|
16020
|
+
`url="${escapeXml3(options.streamUrl)}"`,
|
|
16021
|
+
options.streamName ? `name="${escapeXml3(options.streamName)}"` : undefined,
|
|
16022
|
+
options.track ? `track="${escapeXml3(options.track)}"` : undefined,
|
|
16023
|
+
options.codec ? `codec="${escapeXml3(options.codec)}"` : undefined,
|
|
16024
|
+
options.bidirectionalMode ? `bidirectionalMode="${escapeXml3(options.bidirectionalMode)}"` : undefined,
|
|
16025
|
+
options.bidirectionalCodec ? `bidirectionalCodec="${escapeXml3(options.bidirectionalCodec)}"` : undefined
|
|
16026
|
+
].filter((value) => Boolean(value)).join(" ");
|
|
16027
|
+
return `<?xml version="1.0" encoding="UTF-8"?><Response><Start><Stream ${attributes} /></Start></Response>`;
|
|
16028
|
+
};
|
|
16029
|
+
var decodeBase64 = (value) => Uint8Array.from(Buffer4.from(value, "base64"));
|
|
16030
|
+
var verifyVoiceTelnyxWebhookSignature = async (input) => {
|
|
16031
|
+
if (!input.publicKey) {
|
|
16032
|
+
return { ok: false, reason: "missing-secret" };
|
|
16033
|
+
}
|
|
16034
|
+
const signature = input.headers.get("telnyx-signature-ed25519");
|
|
16035
|
+
const timestamp = input.headers.get("telnyx-timestamp");
|
|
16036
|
+
if (!signature || !timestamp) {
|
|
16037
|
+
return { ok: false, reason: "missing-signature" };
|
|
16038
|
+
}
|
|
16039
|
+
const toleranceSeconds = input.toleranceSeconds ?? 300;
|
|
16040
|
+
const timestampNumber = Number(timestamp);
|
|
16041
|
+
if (Number.isFinite(timestampNumber) && Math.abs(Date.now() / 1000 - timestampNumber) > toleranceSeconds) {
|
|
16042
|
+
return { ok: false, reason: "invalid-signature" };
|
|
16043
|
+
}
|
|
16044
|
+
try {
|
|
16045
|
+
const key = await crypto.subtle.importKey("raw", decodeBase64(input.publicKey), "Ed25519", false, ["verify"]);
|
|
16046
|
+
const ok = await crypto.subtle.verify("Ed25519", key, decodeBase64(signature), new TextEncoder().encode(`${timestamp}|${input.body}`));
|
|
16047
|
+
return ok ? { ok: true } : { ok: false, reason: "invalid-signature" };
|
|
16048
|
+
} catch {
|
|
16049
|
+
return { ok: false, reason: "invalid-signature" };
|
|
16050
|
+
}
|
|
16051
|
+
};
|
|
16052
|
+
var buildTelnyxVoiceSetupStatus = async (options, input) => {
|
|
16053
|
+
const origin = resolveRequestOrigin2(input.request);
|
|
16054
|
+
const stream = await resolveTelnyxStreamUrl(options, input);
|
|
16055
|
+
const texml = joinUrlPath2(origin, input.texmlPath);
|
|
16056
|
+
const webhook = joinUrlPath2(origin, input.webhookPath);
|
|
16057
|
+
const missing = Object.entries(options.setup?.requiredEnv ?? {}).filter((entry) => !entry[1]).map(([name]) => name);
|
|
16058
|
+
const signingConfigured = Boolean(options.webhook?.publicKey || options.webhook?.verify);
|
|
16059
|
+
const warnings = [
|
|
16060
|
+
...stream.startsWith("wss://") ? [] : ["Telnyx media streams should use wss:// in production."],
|
|
16061
|
+
...signingConfigured ? [] : ["Webhook signature verification is not configured."]
|
|
16062
|
+
];
|
|
16063
|
+
return {
|
|
16064
|
+
generatedAt: Date.now(),
|
|
16065
|
+
missing,
|
|
16066
|
+
provider: "telnyx",
|
|
16067
|
+
ready: missing.length === 0 && signingConfigured && warnings.length === 0,
|
|
16068
|
+
signing: {
|
|
16069
|
+
configured: signingConfigured,
|
|
16070
|
+
mode: options.webhook?.verify ? "custom" : options.webhook?.publicKey ? "provider-signature" : "none",
|
|
16071
|
+
verificationUrl: webhook
|
|
16072
|
+
},
|
|
16073
|
+
urls: {
|
|
16074
|
+
stream,
|
|
16075
|
+
texml,
|
|
16076
|
+
webhook
|
|
16077
|
+
},
|
|
16078
|
+
warnings
|
|
16079
|
+
};
|
|
16080
|
+
};
|
|
16081
|
+
var renderTelnyxSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
16082
|
+
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx setup</p>
|
|
16083
|
+
<h1>${escapeHtml18(title)}</h1>
|
|
16084
|
+
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
16085
|
+
<ul>
|
|
16086
|
+
<li><strong>TeXML:</strong> <code>${escapeHtml18(status.urls.texml)}</code></li>
|
|
16087
|
+
<li><strong>Media stream:</strong> <code>${escapeHtml18(status.urls.stream)}</code></li>
|
|
16088
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml18(status.urls.webhook)}</code></li>
|
|
16089
|
+
</ul>
|
|
16090
|
+
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml18(name)}</code></li>`).join("")}</ul>` : ""}
|
|
16091
|
+
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml18(warning)}</li>`).join("")}</ul>` : ""}
|
|
16092
|
+
</main>`;
|
|
16093
|
+
var renderTelnyxSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
16094
|
+
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx smoke test</p>
|
|
16095
|
+
<h1>${escapeHtml18(title)}</h1>
|
|
16096
|
+
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
16097
|
+
<ul>${report.checks.map((check) => `<li><strong>${escapeHtml18(check.name)}</strong>: ${escapeHtml18(check.status)}${check.message ? ` - ${escapeHtml18(check.message)}` : ""}</li>`).join("")}</ul>
|
|
16098
|
+
</main>`;
|
|
16099
|
+
var runTelnyxSmokeTest = async (input) => {
|
|
16100
|
+
const setup = await buildTelnyxVoiceSetupStatus(input.options, input);
|
|
16101
|
+
const checks = [];
|
|
16102
|
+
const texmlResponse = await input.app.handle(new Request(setup.urls.texml));
|
|
16103
|
+
const texml = await texmlResponse.text();
|
|
16104
|
+
const streamUrl = extractTelnyxStreamUrl(texml);
|
|
16105
|
+
checks.push(createSmokeCheck2("texml", texmlResponse.ok && Boolean(streamUrl) ? "pass" : "fail", streamUrl ? "TeXML includes a media stream URL." : 'TeXML is missing <Stream url="...">.', {
|
|
16106
|
+
status: texmlResponse.status,
|
|
16107
|
+
streamUrl
|
|
16108
|
+
}));
|
|
16109
|
+
checks.push(createSmokeCheck2("stream-url", streamUrl?.startsWith("wss://") ? "pass" : "fail", streamUrl?.startsWith("wss://") ? "Media stream URL uses wss://." : "Media stream URL should use wss:// for Telnyx.", {
|
|
16110
|
+
streamUrl
|
|
16111
|
+
}));
|
|
16112
|
+
const webhookBody = {
|
|
16113
|
+
data: {
|
|
16114
|
+
event_type: input.options.smoke?.eventType ?? "call.hangup",
|
|
16115
|
+
id: "telnyx-smoke-event",
|
|
16116
|
+
payload: {
|
|
16117
|
+
call_control_id: input.options.smoke?.callControlId ?? "telnyx-smoke-call",
|
|
16118
|
+
call_leg_id: input.options.smoke?.callLegId ?? "telnyx-smoke-leg",
|
|
16119
|
+
call_session_id: input.options.smoke?.sessionId ?? "telnyx-smoke-session",
|
|
16120
|
+
hangup_cause: "busy",
|
|
16121
|
+
sip_hangup_cause: 486
|
|
16122
|
+
},
|
|
16123
|
+
record_type: "event"
|
|
16124
|
+
}
|
|
16125
|
+
};
|
|
16126
|
+
const webhookResponse = await input.app.handle(new Request(setup.urls.webhook, {
|
|
16127
|
+
body: JSON.stringify(webhookBody),
|
|
16128
|
+
headers: {
|
|
16129
|
+
"content-type": "application/json"
|
|
16130
|
+
},
|
|
16131
|
+
method: "POST"
|
|
16132
|
+
}));
|
|
16133
|
+
const webhookText = await webhookResponse.text();
|
|
16134
|
+
const webhookPayload = (() => {
|
|
16135
|
+
try {
|
|
16136
|
+
return JSON.parse(webhookText);
|
|
16137
|
+
} catch {
|
|
16138
|
+
return webhookText;
|
|
16139
|
+
}
|
|
16140
|
+
})();
|
|
16141
|
+
checks.push(createSmokeCheck2("webhook", webhookResponse.ok ? "pass" : "fail", webhookResponse.ok ? "Synthetic Telnyx event was accepted." : "Synthetic Telnyx event failed.", {
|
|
16142
|
+
status: webhookResponse.status
|
|
16143
|
+
}));
|
|
16144
|
+
for (const warning of setup.warnings) {
|
|
16145
|
+
checks.push(createSmokeCheck2("setup-warning", "warn", warning));
|
|
16146
|
+
}
|
|
16147
|
+
for (const name of setup.missing) {
|
|
16148
|
+
checks.push(createSmokeCheck2("missing-env", "fail", `${name} is missing.`));
|
|
16149
|
+
}
|
|
16150
|
+
const baseReport = {
|
|
16151
|
+
checks,
|
|
16152
|
+
generatedAt: Date.now(),
|
|
16153
|
+
pass: checks.every((check) => check.status !== "fail"),
|
|
16154
|
+
provider: "telnyx",
|
|
16155
|
+
setup,
|
|
16156
|
+
texml: {
|
|
16157
|
+
status: texmlResponse.status,
|
|
16158
|
+
streamUrl
|
|
16159
|
+
},
|
|
16160
|
+
twiml: {
|
|
16161
|
+
status: texmlResponse.status,
|
|
16162
|
+
streamUrl
|
|
16163
|
+
},
|
|
16164
|
+
webhook: {
|
|
16165
|
+
body: webhookPayload,
|
|
16166
|
+
status: webhookResponse.status
|
|
16167
|
+
}
|
|
16168
|
+
};
|
|
16169
|
+
return {
|
|
16170
|
+
...baseReport,
|
|
16171
|
+
contract: evaluateVoiceTelephonyContract({
|
|
16172
|
+
setup,
|
|
16173
|
+
smoke: baseReport
|
|
16174
|
+
})
|
|
16175
|
+
};
|
|
16176
|
+
};
|
|
16177
|
+
var createTelnyxVoiceRoutes = (options = {}) => {
|
|
16178
|
+
const streamPath = options.streamPath ?? "/api/voice/telnyx/stream";
|
|
16179
|
+
const texmlPath = options.texml?.path ?? "/api/voice/telnyx";
|
|
16180
|
+
const webhookPath = options.webhook?.path ?? "/api/voice/telnyx/webhook";
|
|
16181
|
+
const setupPath = options.setup?.path === false ? false : options.setup?.path ?? "/api/voice/telnyx/setup";
|
|
16182
|
+
const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/telnyx/smoke";
|
|
16183
|
+
const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
|
|
16184
|
+
const verify = options.webhook?.verify ?? (options.webhook?.publicKey ? (input) => verifyVoiceTelnyxWebhookSignature({
|
|
16185
|
+
body: input.rawBody,
|
|
16186
|
+
headers: input.headers,
|
|
16187
|
+
publicKey: options.webhook?.publicKey,
|
|
16188
|
+
toleranceSeconds: options.webhook?.toleranceSeconds
|
|
16189
|
+
}) : undefined);
|
|
16190
|
+
const app = new Elysia19({
|
|
16191
|
+
name: options.name ?? "absolutejs-voice-telnyx"
|
|
16192
|
+
}).get(texmlPath, async ({ query, request }) => {
|
|
16193
|
+
const streamUrl = await resolveTelnyxStreamUrl(options, {
|
|
16194
|
+
query,
|
|
16195
|
+
request,
|
|
16196
|
+
streamPath
|
|
16197
|
+
});
|
|
16198
|
+
return new Response(createTelnyxVoiceResponse({
|
|
16199
|
+
...options.texml?.response,
|
|
16200
|
+
streamUrl
|
|
16201
|
+
}), {
|
|
16202
|
+
headers: {
|
|
16203
|
+
"content-type": "text/xml; charset=utf-8"
|
|
16204
|
+
}
|
|
16205
|
+
});
|
|
16206
|
+
}).post(texmlPath, async ({ query, request }) => {
|
|
16207
|
+
const streamUrl = await resolveTelnyxStreamUrl(options, {
|
|
16208
|
+
query,
|
|
16209
|
+
request,
|
|
16210
|
+
streamPath
|
|
16211
|
+
});
|
|
16212
|
+
return new Response(createTelnyxVoiceResponse({
|
|
16213
|
+
...options.texml?.response,
|
|
16214
|
+
streamUrl
|
|
16215
|
+
}), {
|
|
16216
|
+
headers: {
|
|
16217
|
+
"content-type": "text/xml; charset=utf-8"
|
|
16218
|
+
}
|
|
16219
|
+
});
|
|
16220
|
+
}).use(createVoiceTelephonyWebhookRoutes({
|
|
16221
|
+
...options.webhook ?? {},
|
|
16222
|
+
context: options.context,
|
|
16223
|
+
path: webhookPath,
|
|
16224
|
+
policy: webhookPolicy,
|
|
16225
|
+
provider: "telnyx",
|
|
16226
|
+
requireVerification: Boolean(options.webhook?.publicKey),
|
|
16227
|
+
resolveSessionId: options.webhook?.resolveSessionId ?? (({ event }) => {
|
|
16228
|
+
const metadata = event.metadata;
|
|
16229
|
+
return typeof metadata?.call_session_id === "string" ? metadata.call_session_id : typeof metadata?.callSessionId === "string" ? metadata.callSessionId : typeof metadata?.call_control_id === "string" ? metadata.call_control_id : undefined;
|
|
16230
|
+
}),
|
|
16231
|
+
verify
|
|
16232
|
+
}));
|
|
16233
|
+
const withSetup = setupPath ? app.get(setupPath, async ({ query, request }) => {
|
|
16234
|
+
const status = await buildTelnyxVoiceSetupStatus(options, {
|
|
16235
|
+
query,
|
|
16236
|
+
request,
|
|
16237
|
+
streamPath,
|
|
16238
|
+
texmlPath,
|
|
16239
|
+
webhookPath
|
|
16240
|
+
});
|
|
16241
|
+
if (query.format === "html") {
|
|
16242
|
+
return new Response(renderTelnyxSetupHTML(status, options.setup?.title ?? "AbsoluteJS Telnyx Voice Setup"), {
|
|
16243
|
+
headers: {
|
|
16244
|
+
"content-type": "text/html; charset=utf-8"
|
|
16245
|
+
}
|
|
16246
|
+
});
|
|
16247
|
+
}
|
|
16248
|
+
return status;
|
|
16249
|
+
}) : app;
|
|
16250
|
+
if (!smokePath) {
|
|
16251
|
+
return withSetup;
|
|
16252
|
+
}
|
|
16253
|
+
return withSetup.get(smokePath, async ({ query, request }) => {
|
|
16254
|
+
const report = await runTelnyxSmokeTest({
|
|
16255
|
+
app,
|
|
16256
|
+
options,
|
|
16257
|
+
query,
|
|
16258
|
+
request,
|
|
16259
|
+
streamPath,
|
|
16260
|
+
texmlPath,
|
|
16261
|
+
webhookPath
|
|
16262
|
+
});
|
|
16263
|
+
if (query.format === "html") {
|
|
16264
|
+
return new Response(renderTelnyxSmokeHTML(report, options.smoke?.title ?? "AbsoluteJS Telnyx Voice Smoke Test"), {
|
|
16265
|
+
headers: {
|
|
16266
|
+
"content-type": "text/html; charset=utf-8"
|
|
16267
|
+
}
|
|
16268
|
+
});
|
|
16269
|
+
}
|
|
16270
|
+
return report;
|
|
16271
|
+
});
|
|
16272
|
+
};
|
|
16273
|
+
// src/telephony/plivo.ts
|
|
16274
|
+
import { Buffer as Buffer5 } from "buffer";
|
|
16275
|
+
import { Elysia as Elysia20 } from "elysia";
|
|
16276
|
+
var escapeXml4 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
16277
|
+
var escapeHtml19 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
16278
|
+
var joinUrlPath3 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
16279
|
+
var resolveRequestOrigin3 = (request) => {
|
|
16280
|
+
const url = new URL(request.url);
|
|
16281
|
+
const forwardedHost = request.headers.get("x-forwarded-host");
|
|
16282
|
+
const forwardedProto = request.headers.get("x-forwarded-proto");
|
|
16283
|
+
const host = forwardedHost ?? request.headers.get("host") ?? url.host;
|
|
16284
|
+
const protocol = forwardedProto ?? url.protocol.replace(":", "");
|
|
16285
|
+
return `${protocol}://${host}`;
|
|
16286
|
+
};
|
|
16287
|
+
var boolAttr = (value) => typeof value === "boolean" ? String(value) : undefined;
|
|
16288
|
+
var extraHeadersAttr = (headers) => {
|
|
16289
|
+
if (!headers || typeof headers === "string") {
|
|
16290
|
+
return headers;
|
|
16291
|
+
}
|
|
16292
|
+
return Object.entries(headers).filter((entry) => entry[1] !== undefined).map(([key, value]) => `${key}=${String(value)}`).join(",");
|
|
16293
|
+
};
|
|
16294
|
+
var extractPlivoStreamUrl = (xml) => xml.match(/<Stream\b[^>]*>([^<]+)<\/Stream>/i)?.[1]?.trim();
|
|
16295
|
+
var createSmokeCheck3 = (name, status, message, details) => ({
|
|
16296
|
+
details,
|
|
16297
|
+
message,
|
|
16298
|
+
name,
|
|
16299
|
+
status
|
|
16300
|
+
});
|
|
16301
|
+
var resolvePlivoStreamUrl = async (options, input) => {
|
|
16302
|
+
if (typeof options.answer?.streamUrl === "function") {
|
|
16303
|
+
return options.answer.streamUrl(input);
|
|
16304
|
+
}
|
|
16305
|
+
if (typeof options.answer?.streamUrl === "string") {
|
|
16306
|
+
return options.answer.streamUrl;
|
|
16307
|
+
}
|
|
16308
|
+
const origin = resolveRequestOrigin3(input.request);
|
|
16309
|
+
const wsOrigin = origin.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
|
|
16310
|
+
return `${wsOrigin}${input.streamPath}`;
|
|
16311
|
+
};
|
|
16312
|
+
var createPlivoVoiceResponse = (options) => {
|
|
16313
|
+
const attributes = [
|
|
16314
|
+
options.bidirectional !== undefined ? `bidirectional="${escapeXml4(String(options.bidirectional))}"` : undefined,
|
|
16315
|
+
options.audioTrack ? `audioTrack="${escapeXml4(options.audioTrack)}"` : undefined,
|
|
16316
|
+
options.streamTimeout ? `streamTimeout="${escapeXml4(String(options.streamTimeout))}"` : undefined,
|
|
16317
|
+
options.contentType ? `contentType="${escapeXml4(options.contentType)}"` : undefined,
|
|
16318
|
+
options.keepCallAlive !== undefined ? `keepCallAlive="${escapeXml4(String(options.keepCallAlive))}"` : undefined,
|
|
16319
|
+
extraHeadersAttr(options.extraHeaders) ? `extraHeaders="${escapeXml4(extraHeadersAttr(options.extraHeaders))}"` : undefined,
|
|
16320
|
+
options.statusCallbackUrl ? `statusCallbackUrl="${escapeXml4(options.statusCallbackUrl)}"` : undefined,
|
|
16321
|
+
options.statusCallbackMethod ? `statusCallbackMethod="${escapeXml4(options.statusCallbackMethod)}"` : undefined,
|
|
16322
|
+
boolAttr(options.noiseCancellation) ? `noiseCancellation="${escapeXml4(boolAttr(options.noiseCancellation))}"` : undefined,
|
|
16323
|
+
options.noiseCancellationLevel ? `noiseCancellationLevel="${escapeXml4(String(options.noiseCancellationLevel))}"` : undefined
|
|
16324
|
+
].filter((value) => Boolean(value)).join(" ");
|
|
16325
|
+
const openTag = attributes ? `<Stream ${attributes}>` : "<Stream>";
|
|
16326
|
+
return `<?xml version="1.0" encoding="UTF-8"?><Response>${openTag}${escapeXml4(options.streamUrl)}</Stream></Response>`;
|
|
16327
|
+
};
|
|
16328
|
+
var toBase642 = (bytes) => Buffer5.from(new Uint8Array(bytes)).toString("base64");
|
|
16329
|
+
var timingSafeEqual3 = (left, right) => {
|
|
16330
|
+
const encoder = new TextEncoder;
|
|
16331
|
+
const leftBytes = encoder.encode(left);
|
|
16332
|
+
const rightBytes = encoder.encode(right);
|
|
16333
|
+
if (leftBytes.length !== rightBytes.length) {
|
|
16334
|
+
return false;
|
|
16335
|
+
}
|
|
16336
|
+
let diff = 0;
|
|
16337
|
+
for (let index = 0;index < leftBytes.length; index += 1) {
|
|
16338
|
+
diff |= leftBytes[index] ^ rightBytes[index];
|
|
16339
|
+
}
|
|
16340
|
+
return diff === 0;
|
|
16341
|
+
};
|
|
16342
|
+
var signHmacSHA256Base64 = async (secret, payload) => {
|
|
16343
|
+
const encoder = new TextEncoder;
|
|
16344
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
16345
|
+
hash: "SHA-256",
|
|
16346
|
+
name: "HMAC"
|
|
16347
|
+
}, false, ["sign"]);
|
|
16348
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
16349
|
+
return toBase642(signature);
|
|
16350
|
+
};
|
|
16351
|
+
var sortedParamsForSignature2 = (body) => {
|
|
16352
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
16353
|
+
return "";
|
|
16354
|
+
}
|
|
16355
|
+
return Object.entries(body).filter(([, value]) => value !== undefined && value !== null).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}${String(value)}`).join("");
|
|
16356
|
+
};
|
|
16357
|
+
var signVoicePlivoWebhook = async (input) => signHmacSHA256Base64(input.authToken, `${input.url}${sortedParamsForSignature2(input.body)}.${input.nonce}`);
|
|
16358
|
+
var headerList = (value) => value?.split(",").map((signature) => signature.trim()).filter(Boolean) ?? [];
|
|
16359
|
+
var verifyVoicePlivoWebhookSignature = async (input) => {
|
|
16360
|
+
if (!input.authToken) {
|
|
16361
|
+
return { ok: false, reason: "missing-secret" };
|
|
16362
|
+
}
|
|
16363
|
+
const nonce = input.headers.get("x-plivo-signature-v3-nonce");
|
|
16364
|
+
const signatures = [
|
|
16365
|
+
...headerList(input.headers.get("x-plivo-signature-v3")),
|
|
16366
|
+
...headerList(input.headers.get("x-plivo-signature-ma-v3"))
|
|
16367
|
+
];
|
|
16368
|
+
if (!nonce || signatures.length === 0) {
|
|
16369
|
+
return { ok: false, reason: "missing-signature" };
|
|
16370
|
+
}
|
|
16371
|
+
const expected = await signVoicePlivoWebhook({
|
|
16372
|
+
authToken: input.authToken,
|
|
16373
|
+
body: input.body,
|
|
16374
|
+
nonce,
|
|
16375
|
+
url: input.url
|
|
16376
|
+
});
|
|
16377
|
+
return signatures.some((signature) => timingSafeEqual3(signature, expected)) ? { ok: true } : { ok: false, reason: "invalid-signature" };
|
|
16378
|
+
};
|
|
16379
|
+
var buildPlivoVoiceSetupStatus = async (options, input) => {
|
|
16380
|
+
const origin = resolveRequestOrigin3(input.request);
|
|
16381
|
+
const stream = await resolvePlivoStreamUrl(options, input);
|
|
16382
|
+
const answer = joinUrlPath3(origin, input.answerPath);
|
|
16383
|
+
const webhook = joinUrlPath3(origin, input.webhookPath);
|
|
16384
|
+
const missing = Object.entries(options.setup?.requiredEnv ?? {}).filter((entry) => !entry[1]).map(([name]) => name);
|
|
16385
|
+
const signingConfigured = Boolean(options.webhook?.authToken || options.webhook?.verify);
|
|
16386
|
+
const warnings = [
|
|
16387
|
+
...stream.startsWith("wss://") ? [] : ["Plivo audio streams should use wss:// in production."],
|
|
16388
|
+
...signingConfigured ? [] : ["Webhook signature verification is not configured."]
|
|
16389
|
+
];
|
|
16390
|
+
return {
|
|
16391
|
+
generatedAt: Date.now(),
|
|
16392
|
+
missing,
|
|
16393
|
+
provider: "plivo",
|
|
16394
|
+
ready: missing.length === 0 && signingConfigured && warnings.length === 0,
|
|
16395
|
+
signing: {
|
|
16396
|
+
configured: signingConfigured,
|
|
16397
|
+
mode: options.webhook?.verify ? "custom" : options.webhook?.authToken ? "provider-signature" : "none",
|
|
16398
|
+
verificationUrl: webhook
|
|
16399
|
+
},
|
|
16400
|
+
urls: {
|
|
16401
|
+
answer,
|
|
16402
|
+
stream,
|
|
16403
|
+
twiml: answer,
|
|
16404
|
+
webhook
|
|
16405
|
+
},
|
|
16406
|
+
warnings
|
|
16407
|
+
};
|
|
16408
|
+
};
|
|
16409
|
+
var renderPlivoSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
16410
|
+
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo setup</p>
|
|
16411
|
+
<h1>${escapeHtml19(title)}</h1>
|
|
16412
|
+
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
16413
|
+
<ul>
|
|
16414
|
+
<li><strong>Answer XML:</strong> <code>${escapeHtml19(status.urls.answer)}</code></li>
|
|
16415
|
+
<li><strong>Audio stream:</strong> <code>${escapeHtml19(status.urls.stream)}</code></li>
|
|
16416
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml19(status.urls.webhook)}</code></li>
|
|
16417
|
+
</ul>
|
|
16418
|
+
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml19(name)}</code></li>`).join("")}</ul>` : ""}
|
|
16419
|
+
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml19(warning)}</li>`).join("")}</ul>` : ""}
|
|
16420
|
+
</main>`;
|
|
16421
|
+
var renderPlivoSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
16422
|
+
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo smoke test</p>
|
|
16423
|
+
<h1>${escapeHtml19(title)}</h1>
|
|
16424
|
+
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
16425
|
+
<ul>${report.checks.map((check) => `<li><strong>${escapeHtml19(check.name)}</strong>: ${escapeHtml19(check.status)}${check.message ? ` - ${escapeHtml19(check.message)}` : ""}</li>`).join("")}</ul>
|
|
16426
|
+
</main>`;
|
|
16427
|
+
var runPlivoSmokeTest = async (input) => {
|
|
16428
|
+
const setup = await buildPlivoVoiceSetupStatus(input.options, input);
|
|
16429
|
+
const checks = [];
|
|
16430
|
+
const answerResponse = await input.app.handle(new Request(setup.urls.answer));
|
|
16431
|
+
const answerXml = await answerResponse.text();
|
|
16432
|
+
const streamUrl = extractPlivoStreamUrl(answerXml);
|
|
16433
|
+
checks.push(createSmokeCheck3("answer-xml", answerResponse.ok && Boolean(streamUrl) ? "pass" : "fail", streamUrl ? "Answer XML includes a Stream URL." : "Answer XML is missing <Stream>...</Stream>.", {
|
|
16434
|
+
status: answerResponse.status,
|
|
16435
|
+
streamUrl
|
|
16436
|
+
}));
|
|
16437
|
+
checks.push(createSmokeCheck3("stream-url", streamUrl?.startsWith("wss://") ? "pass" : "fail", streamUrl?.startsWith("wss://") ? "Audio stream URL uses wss://." : "Audio stream URL should use wss:// for Plivo.", {
|
|
16438
|
+
streamUrl
|
|
16439
|
+
}));
|
|
16440
|
+
const webhookBody = new URLSearchParams({
|
|
16441
|
+
CallUUID: input.options.smoke?.callUuid ?? "plivo-smoke-call",
|
|
16442
|
+
Duration: "0",
|
|
16443
|
+
Event: input.options.smoke?.eventType ?? "Hangup",
|
|
16444
|
+
From: "+15555550100",
|
|
16445
|
+
HangupCause: "busy",
|
|
16446
|
+
SessionId: input.options.smoke?.sessionId ?? "plivo-smoke-session",
|
|
16447
|
+
SipResponseCode: String(input.options.smoke?.sipCode ?? 486),
|
|
16448
|
+
To: "+15555550101",
|
|
16449
|
+
status: input.options.smoke?.status ?? "busy"
|
|
16450
|
+
});
|
|
16451
|
+
const webhookResponse = await input.app.handle(new Request(setup.urls.webhook, {
|
|
16452
|
+
body: webhookBody,
|
|
16453
|
+
headers: {
|
|
16454
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
16455
|
+
},
|
|
16456
|
+
method: "POST"
|
|
16457
|
+
}));
|
|
16458
|
+
const webhookText = await webhookResponse.text();
|
|
16459
|
+
const webhookPayload = (() => {
|
|
16460
|
+
try {
|
|
16461
|
+
return JSON.parse(webhookText);
|
|
16462
|
+
} catch {
|
|
16463
|
+
return webhookText;
|
|
16464
|
+
}
|
|
16465
|
+
})();
|
|
16466
|
+
checks.push(createSmokeCheck3("webhook", webhookResponse.ok ? "pass" : "fail", webhookResponse.ok ? "Synthetic Plivo event was accepted." : "Synthetic Plivo event failed.", {
|
|
16467
|
+
status: webhookResponse.status
|
|
16468
|
+
}));
|
|
16469
|
+
for (const warning of setup.warnings) {
|
|
16470
|
+
checks.push(createSmokeCheck3("setup-warning", "warn", warning));
|
|
16471
|
+
}
|
|
16472
|
+
for (const name of setup.missing) {
|
|
16473
|
+
checks.push(createSmokeCheck3("missing-env", "fail", `${name} is missing.`));
|
|
16474
|
+
}
|
|
16475
|
+
const baseReport = {
|
|
16476
|
+
answer: {
|
|
16477
|
+
status: answerResponse.status,
|
|
16478
|
+
streamUrl
|
|
16479
|
+
},
|
|
16480
|
+
checks,
|
|
16481
|
+
generatedAt: Date.now(),
|
|
16482
|
+
pass: checks.every((check) => check.status !== "fail"),
|
|
16483
|
+
provider: "plivo",
|
|
16484
|
+
setup,
|
|
16485
|
+
twiml: {
|
|
16486
|
+
status: answerResponse.status,
|
|
16487
|
+
streamUrl
|
|
16488
|
+
},
|
|
16489
|
+
webhook: {
|
|
16490
|
+
body: webhookPayload,
|
|
16491
|
+
status: webhookResponse.status
|
|
16492
|
+
}
|
|
16493
|
+
};
|
|
16494
|
+
return {
|
|
16495
|
+
...baseReport,
|
|
16496
|
+
contract: evaluateVoiceTelephonyContract({
|
|
16497
|
+
setup,
|
|
16498
|
+
smoke: baseReport
|
|
16499
|
+
})
|
|
16500
|
+
};
|
|
16501
|
+
};
|
|
16502
|
+
var createPlivoVoiceRoutes = (options = {}) => {
|
|
16503
|
+
const streamPath = options.streamPath ?? "/api/voice/plivo/stream";
|
|
16504
|
+
const answerPath = options.answer?.path ?? "/api/voice/plivo";
|
|
16505
|
+
const webhookPath = options.webhook?.path ?? "/api/voice/plivo/webhook";
|
|
16506
|
+
const setupPath = options.setup?.path === false ? false : options.setup?.path ?? "/api/voice/plivo/setup";
|
|
16507
|
+
const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/plivo/smoke";
|
|
16508
|
+
const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
|
|
16509
|
+
const verificationUrl = options.webhook?.verificationUrl;
|
|
16510
|
+
const verify = options.webhook?.verify ?? (options.webhook?.authToken ? (input) => verifyVoicePlivoWebhookSignature({
|
|
16511
|
+
authToken: options.webhook?.authToken,
|
|
16512
|
+
body: input.body,
|
|
16513
|
+
headers: input.request.headers,
|
|
16514
|
+
url: typeof verificationUrl === "function" ? verificationUrl({
|
|
16515
|
+
query: input.query,
|
|
16516
|
+
request: input.request
|
|
16517
|
+
}) : verificationUrl ?? input.request.url
|
|
16518
|
+
}) : undefined);
|
|
16519
|
+
const app = new Elysia20({
|
|
16520
|
+
name: options.name ?? "absolutejs-voice-plivo"
|
|
16521
|
+
}).get(answerPath, async ({ query, request }) => {
|
|
16522
|
+
const streamUrl = await resolvePlivoStreamUrl(options, {
|
|
16523
|
+
query,
|
|
16524
|
+
request,
|
|
16525
|
+
streamPath
|
|
16526
|
+
});
|
|
16527
|
+
return new Response(createPlivoVoiceResponse({
|
|
16528
|
+
...options.answer?.response,
|
|
16529
|
+
streamUrl
|
|
16530
|
+
}), {
|
|
16531
|
+
headers: {
|
|
16532
|
+
"content-type": "text/xml; charset=utf-8"
|
|
16533
|
+
}
|
|
16534
|
+
});
|
|
16535
|
+
}).post(answerPath, async ({ query, request }) => {
|
|
16536
|
+
const streamUrl = await resolvePlivoStreamUrl(options, {
|
|
16537
|
+
query,
|
|
16538
|
+
request,
|
|
16539
|
+
streamPath
|
|
16540
|
+
});
|
|
16541
|
+
return new Response(createPlivoVoiceResponse({
|
|
16542
|
+
...options.answer?.response,
|
|
16543
|
+
streamUrl
|
|
16544
|
+
}), {
|
|
16545
|
+
headers: {
|
|
16546
|
+
"content-type": "text/xml; charset=utf-8"
|
|
16547
|
+
}
|
|
16548
|
+
});
|
|
16549
|
+
}).use(createVoiceTelephonyWebhookRoutes({
|
|
16550
|
+
...options.webhook ?? {},
|
|
16551
|
+
context: options.context,
|
|
16552
|
+
path: webhookPath,
|
|
16553
|
+
policy: webhookPolicy,
|
|
16554
|
+
provider: "plivo",
|
|
16555
|
+
requireVerification: Boolean(options.webhook?.authToken),
|
|
16556
|
+
resolveSessionId: options.webhook?.resolveSessionId ?? (({ event }) => {
|
|
16557
|
+
const metadata = event.metadata;
|
|
16558
|
+
return typeof metadata?.SessionId === "string" ? metadata.SessionId : typeof metadata?.sessionId === "string" ? metadata.sessionId : typeof metadata?.CallUUID === "string" ? metadata.CallUUID : typeof metadata?.call_uuid === "string" ? metadata.call_uuid : undefined;
|
|
16559
|
+
}),
|
|
16560
|
+
verify
|
|
16561
|
+
}));
|
|
16562
|
+
const withSetup = setupPath ? app.get(setupPath, async ({ query, request }) => {
|
|
16563
|
+
const status = await buildPlivoVoiceSetupStatus(options, {
|
|
16564
|
+
answerPath,
|
|
16565
|
+
query,
|
|
16566
|
+
request,
|
|
16567
|
+
streamPath,
|
|
16568
|
+
webhookPath
|
|
16569
|
+
});
|
|
16570
|
+
if (query.format === "html") {
|
|
16571
|
+
return new Response(renderPlivoSetupHTML(status, options.setup?.title ?? "AbsoluteJS Plivo Voice Setup"), {
|
|
16572
|
+
headers: {
|
|
16573
|
+
"content-type": "text/html; charset=utf-8"
|
|
16574
|
+
}
|
|
16575
|
+
});
|
|
16576
|
+
}
|
|
16577
|
+
return status;
|
|
16578
|
+
}) : app;
|
|
16579
|
+
if (!smokePath) {
|
|
16580
|
+
return withSetup;
|
|
16581
|
+
}
|
|
16582
|
+
return withSetup.get(smokePath, async ({ query, request }) => {
|
|
16583
|
+
const report = await runPlivoSmokeTest({
|
|
16584
|
+
answerPath,
|
|
16585
|
+
app,
|
|
16586
|
+
options,
|
|
16587
|
+
query,
|
|
16588
|
+
request,
|
|
16589
|
+
streamPath,
|
|
16590
|
+
webhookPath
|
|
16591
|
+
});
|
|
16592
|
+
if (query.format === "html") {
|
|
16593
|
+
return new Response(renderPlivoSmokeHTML(report, options.smoke?.title ?? "AbsoluteJS Plivo Voice Smoke Test"), {
|
|
16594
|
+
headers: {
|
|
16595
|
+
"content-type": "text/html; charset=utf-8"
|
|
16596
|
+
}
|
|
16597
|
+
});
|
|
16598
|
+
}
|
|
16599
|
+
return report;
|
|
16600
|
+
});
|
|
16601
|
+
};
|
|
15986
16602
|
// src/telephony/response.ts
|
|
15987
16603
|
var normalizeWhitespace = (value) => value.replace(/\s+/g, " ").trim();
|
|
15988
16604
|
var DEFAULT_MAX_WORDS = 12;
|
|
@@ -16041,6 +16657,8 @@ export {
|
|
|
16041
16657
|
voiceTelephonyOutcomeToRouteResult,
|
|
16042
16658
|
voice,
|
|
16043
16659
|
verifyVoiceTwilioWebhookSignature,
|
|
16660
|
+
verifyVoiceTelnyxWebhookSignature,
|
|
16661
|
+
verifyVoicePlivoWebhookSignature,
|
|
16044
16662
|
verifyVoiceOpsWebhookSignature,
|
|
16045
16663
|
validateVoiceWorkflowRouteResult,
|
|
16046
16664
|
transcodeTwilioInboundPayloadToPCM16,
|
|
@@ -16064,6 +16682,7 @@ export {
|
|
|
16064
16682
|
summarizeVoiceAppKitStatus,
|
|
16065
16683
|
startVoiceOpsTask,
|
|
16066
16684
|
signVoiceTwilioWebhook,
|
|
16685
|
+
signVoicePlivoWebhook,
|
|
16067
16686
|
shapeTelephonyAssistantText,
|
|
16068
16687
|
selectVoiceTraceEventsForPrune,
|
|
16069
16688
|
runVoiceToolContractSuite,
|
|
@@ -16282,11 +16901,15 @@ export {
|
|
|
16282
16901
|
createTwilioVoiceRoutes,
|
|
16283
16902
|
createTwilioVoiceResponse,
|
|
16284
16903
|
createTwilioMediaStreamBridge,
|
|
16904
|
+
createTelnyxVoiceRoutes,
|
|
16905
|
+
createTelnyxVoiceResponse,
|
|
16285
16906
|
createStoredVoiceOpsTask,
|
|
16286
16907
|
createStoredVoiceIntegrationEvent,
|
|
16287
16908
|
createStoredVoiceExternalObjectMap,
|
|
16288
16909
|
createStoredVoiceCallReviewArtifact,
|
|
16289
16910
|
createRiskyTurnCorrectionHandler,
|
|
16911
|
+
createPlivoVoiceRoutes,
|
|
16912
|
+
createPlivoVoiceResponse,
|
|
16290
16913
|
createPhraseHintCorrectionHandler,
|
|
16291
16914
|
createOpenAIVoiceAssistantModel,
|
|
16292
16915
|
createMemoryVoiceTelephonyWebhookIdempotencyStore,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { type VoiceTelephonyContractReport, type VoiceTelephonySetupStatus, type VoiceTelephonySmokeCheck, type VoiceTelephonySmokeReport } from './contract';
|
|
3
|
+
import { type VoiceTelephonyOutcomePolicy, type VoiceTelephonyWebhookRoutesOptions, type VoiceTelephonyWebhookVerificationResult } from '../telephonyOutcome';
|
|
4
|
+
import type { VoiceSessionRecord } from '../types';
|
|
5
|
+
export type PlivoVoiceResponseOptions = {
|
|
6
|
+
audioTrack?: 'both' | 'inbound' | 'outbound';
|
|
7
|
+
bidirectional?: boolean;
|
|
8
|
+
contentType?: 'audio/x-l16;rate=8000' | 'audio/x-l16;rate=16000' | 'audio/x-mulaw;rate=8000';
|
|
9
|
+
extraHeaders?: Record<string, string | number | boolean | undefined> | string;
|
|
10
|
+
keepCallAlive?: boolean;
|
|
11
|
+
noiseCancellation?: boolean;
|
|
12
|
+
noiseCancellationLevel?: number;
|
|
13
|
+
statusCallbackMethod?: 'GET' | 'POST';
|
|
14
|
+
statusCallbackUrl?: string;
|
|
15
|
+
streamTimeout?: number;
|
|
16
|
+
streamUrl: string;
|
|
17
|
+
};
|
|
18
|
+
export type PlivoVoiceSetupStatus = VoiceTelephonySetupStatus<'plivo'> & {
|
|
19
|
+
urls: VoiceTelephonySetupStatus<'plivo'>['urls'] & {
|
|
20
|
+
answer: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type PlivoVoiceSetupOptions = {
|
|
24
|
+
path?: false | string;
|
|
25
|
+
requiredEnv?: Record<string, string | undefined>;
|
|
26
|
+
title?: string;
|
|
27
|
+
};
|
|
28
|
+
export type PlivoVoiceSmokeCheck = VoiceTelephonySmokeCheck;
|
|
29
|
+
export type PlivoVoiceSmokeReport = VoiceTelephonySmokeReport<'plivo'> & {
|
|
30
|
+
answer?: {
|
|
31
|
+
status: number;
|
|
32
|
+
streamUrl?: string;
|
|
33
|
+
};
|
|
34
|
+
contract: VoiceTelephonyContractReport<'plivo'>;
|
|
35
|
+
setup: PlivoVoiceSetupStatus;
|
|
36
|
+
};
|
|
37
|
+
export type PlivoVoiceSmokeOptions = {
|
|
38
|
+
callUuid?: string;
|
|
39
|
+
eventType?: string;
|
|
40
|
+
path?: false | string;
|
|
41
|
+
sessionId?: string;
|
|
42
|
+
sipCode?: number;
|
|
43
|
+
status?: string;
|
|
44
|
+
title?: string;
|
|
45
|
+
};
|
|
46
|
+
export type PlivoVoiceRoutesOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
|
|
47
|
+
answer?: {
|
|
48
|
+
path?: string;
|
|
49
|
+
response?: Omit<PlivoVoiceResponseOptions, 'streamUrl'>;
|
|
50
|
+
streamUrl?: string | ((input: {
|
|
51
|
+
query: Record<string, unknown>;
|
|
52
|
+
request: Request;
|
|
53
|
+
streamPath: string;
|
|
54
|
+
}) => Promise<string> | string);
|
|
55
|
+
};
|
|
56
|
+
context?: TContext;
|
|
57
|
+
name?: string;
|
|
58
|
+
outcomePolicy?: VoiceTelephonyOutcomePolicy;
|
|
59
|
+
setup?: PlivoVoiceSetupOptions;
|
|
60
|
+
smoke?: PlivoVoiceSmokeOptions;
|
|
61
|
+
streamPath?: string;
|
|
62
|
+
webhook?: Omit<VoiceTelephonyWebhookRoutesOptions<TContext, TSession, TResult>, 'context' | 'path' | 'policy' | 'provider'> & {
|
|
63
|
+
authToken?: string;
|
|
64
|
+
path?: string;
|
|
65
|
+
policy?: VoiceTelephonyOutcomePolicy;
|
|
66
|
+
verificationUrl?: string | ((input: {
|
|
67
|
+
query: Record<string, unknown>;
|
|
68
|
+
request: Request;
|
|
69
|
+
}) => string);
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
export declare const createPlivoVoiceResponse: (options: PlivoVoiceResponseOptions) => string;
|
|
73
|
+
export declare const signVoicePlivoWebhook: (input: {
|
|
74
|
+
authToken: string;
|
|
75
|
+
body?: unknown;
|
|
76
|
+
nonce: string;
|
|
77
|
+
url: string;
|
|
78
|
+
}) => Promise<string>;
|
|
79
|
+
export declare const verifyVoicePlivoWebhookSignature: (input: {
|
|
80
|
+
authToken?: string;
|
|
81
|
+
body?: unknown;
|
|
82
|
+
headers: Headers;
|
|
83
|
+
url: string;
|
|
84
|
+
}) => Promise<VoiceTelephonyWebhookVerificationResult>;
|
|
85
|
+
export declare const createPlivoVoiceRoutes: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options?: PlivoVoiceRoutesOptions<TContext, TSession, TResult>) => Elysia<"", {
|
|
86
|
+
decorator: {};
|
|
87
|
+
store: {};
|
|
88
|
+
derive: {};
|
|
89
|
+
resolve: {};
|
|
90
|
+
}, {
|
|
91
|
+
typebox: {};
|
|
92
|
+
error: {};
|
|
93
|
+
}, {
|
|
94
|
+
schema: {};
|
|
95
|
+
standaloneSchema: {};
|
|
96
|
+
macro: {};
|
|
97
|
+
macroFn: {};
|
|
98
|
+
parser: {};
|
|
99
|
+
response: {};
|
|
100
|
+
}, {
|
|
101
|
+
[x: string]: {
|
|
102
|
+
get: {
|
|
103
|
+
body: unknown;
|
|
104
|
+
params: {};
|
|
105
|
+
query: unknown;
|
|
106
|
+
headers: unknown;
|
|
107
|
+
response: {
|
|
108
|
+
200: Response;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
} & {
|
|
113
|
+
[x: string]: {
|
|
114
|
+
post: {
|
|
115
|
+
body: unknown;
|
|
116
|
+
params: {};
|
|
117
|
+
query: unknown;
|
|
118
|
+
headers: unknown;
|
|
119
|
+
response: {
|
|
120
|
+
200: Response;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
} & {
|
|
125
|
+
[x: string]: {
|
|
126
|
+
post: {
|
|
127
|
+
body: unknown;
|
|
128
|
+
params: {};
|
|
129
|
+
query: unknown;
|
|
130
|
+
headers: unknown;
|
|
131
|
+
response: {
|
|
132
|
+
200: Response | import("..").VoiceTelephonyWebhookDecision<TResult>;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
}, {
|
|
137
|
+
derive: {};
|
|
138
|
+
resolve: {};
|
|
139
|
+
schema: {};
|
|
140
|
+
standaloneSchema: {};
|
|
141
|
+
response: {};
|
|
142
|
+
}, {
|
|
143
|
+
derive: {};
|
|
144
|
+
resolve: {};
|
|
145
|
+
schema: {};
|
|
146
|
+
standaloneSchema: {};
|
|
147
|
+
response: {};
|
|
148
|
+
} & {
|
|
149
|
+
derive: {};
|
|
150
|
+
resolve: {};
|
|
151
|
+
schema: {};
|
|
152
|
+
standaloneSchema: {};
|
|
153
|
+
response: {};
|
|
154
|
+
}>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { type VoiceTelephonyContractReport, type VoiceTelephonySetupStatus, type VoiceTelephonySmokeCheck, type VoiceTelephonySmokeReport } from './contract';
|
|
3
|
+
import { type VoiceTelephonyOutcomePolicy, type VoiceTelephonyWebhookRoutesOptions, type VoiceTelephonyWebhookVerificationResult } from '../telephonyOutcome';
|
|
4
|
+
import type { VoiceSessionRecord } from '../types';
|
|
5
|
+
export type TelnyxVoiceResponseOptions = {
|
|
6
|
+
bidirectionalCodec?: 'AMR-WB' | 'G722' | 'OPUS' | 'PCMA' | 'PCMU';
|
|
7
|
+
bidirectionalMode?: 'mp3' | 'rtp';
|
|
8
|
+
codec?: 'AMR-WB' | 'G722' | 'OPUS' | 'PCMA' | 'PCMU' | 'default';
|
|
9
|
+
streamName?: string;
|
|
10
|
+
streamUrl: string;
|
|
11
|
+
track?: 'both_tracks' | 'inbound_track' | 'outbound_track';
|
|
12
|
+
};
|
|
13
|
+
export type TelnyxVoiceSetupStatus = VoiceTelephonySetupStatus<'telnyx'> & {
|
|
14
|
+
urls: VoiceTelephonySetupStatus<'telnyx'>['urls'] & {
|
|
15
|
+
texml: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export type TelnyxVoiceSetupOptions = {
|
|
19
|
+
path?: false | string;
|
|
20
|
+
requiredEnv?: Record<string, string | undefined>;
|
|
21
|
+
title?: string;
|
|
22
|
+
};
|
|
23
|
+
export type TelnyxVoiceSmokeCheck = VoiceTelephonySmokeCheck;
|
|
24
|
+
export type TelnyxVoiceSmokeReport = VoiceTelephonySmokeReport<'telnyx'> & {
|
|
25
|
+
contract: VoiceTelephonyContractReport<'telnyx'>;
|
|
26
|
+
setup: TelnyxVoiceSetupStatus;
|
|
27
|
+
texml?: {
|
|
28
|
+
status: number;
|
|
29
|
+
streamUrl?: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export type TelnyxVoiceSmokeOptions = {
|
|
33
|
+
callControlId?: string;
|
|
34
|
+
callLegId?: string;
|
|
35
|
+
eventType?: string;
|
|
36
|
+
path?: false | string;
|
|
37
|
+
sessionId?: string;
|
|
38
|
+
title?: string;
|
|
39
|
+
};
|
|
40
|
+
export type TelnyxVoiceRoutesOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
|
|
41
|
+
context?: TContext;
|
|
42
|
+
name?: string;
|
|
43
|
+
outcomePolicy?: VoiceTelephonyOutcomePolicy;
|
|
44
|
+
setup?: TelnyxVoiceSetupOptions;
|
|
45
|
+
smoke?: TelnyxVoiceSmokeOptions;
|
|
46
|
+
streamPath?: string;
|
|
47
|
+
texml?: {
|
|
48
|
+
path?: string;
|
|
49
|
+
response?: Omit<TelnyxVoiceResponseOptions, 'streamUrl'>;
|
|
50
|
+
streamUrl?: string | ((input: {
|
|
51
|
+
query: Record<string, unknown>;
|
|
52
|
+
request: Request;
|
|
53
|
+
streamPath: string;
|
|
54
|
+
}) => Promise<string> | string);
|
|
55
|
+
};
|
|
56
|
+
webhook?: Omit<VoiceTelephonyWebhookRoutesOptions<TContext, TSession, TResult>, 'context' | 'path' | 'policy' | 'provider'> & {
|
|
57
|
+
path?: string;
|
|
58
|
+
policy?: VoiceTelephonyOutcomePolicy;
|
|
59
|
+
publicKey?: string;
|
|
60
|
+
toleranceSeconds?: number;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
export declare const createTelnyxVoiceResponse: (options: TelnyxVoiceResponseOptions) => string;
|
|
64
|
+
export declare const verifyVoiceTelnyxWebhookSignature: (input: {
|
|
65
|
+
body: string;
|
|
66
|
+
headers: Headers;
|
|
67
|
+
publicKey?: string;
|
|
68
|
+
toleranceSeconds?: number;
|
|
69
|
+
}) => Promise<VoiceTelephonyWebhookVerificationResult>;
|
|
70
|
+
export declare const createTelnyxVoiceRoutes: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options?: TelnyxVoiceRoutesOptions<TContext, TSession, TResult>) => Elysia<"", {
|
|
71
|
+
decorator: {};
|
|
72
|
+
store: {};
|
|
73
|
+
derive: {};
|
|
74
|
+
resolve: {};
|
|
75
|
+
}, {
|
|
76
|
+
typebox: {};
|
|
77
|
+
error: {};
|
|
78
|
+
}, {
|
|
79
|
+
schema: {};
|
|
80
|
+
standaloneSchema: {};
|
|
81
|
+
macro: {};
|
|
82
|
+
macroFn: {};
|
|
83
|
+
parser: {};
|
|
84
|
+
response: {};
|
|
85
|
+
}, {
|
|
86
|
+
[x: string]: {
|
|
87
|
+
get: {
|
|
88
|
+
body: unknown;
|
|
89
|
+
params: {};
|
|
90
|
+
query: unknown;
|
|
91
|
+
headers: unknown;
|
|
92
|
+
response: {
|
|
93
|
+
200: Response;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
} & {
|
|
98
|
+
[x: string]: {
|
|
99
|
+
post: {
|
|
100
|
+
body: unknown;
|
|
101
|
+
params: {};
|
|
102
|
+
query: unknown;
|
|
103
|
+
headers: unknown;
|
|
104
|
+
response: {
|
|
105
|
+
200: Response;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
} & {
|
|
110
|
+
[x: string]: {
|
|
111
|
+
post: {
|
|
112
|
+
body: unknown;
|
|
113
|
+
params: {};
|
|
114
|
+
query: unknown;
|
|
115
|
+
headers: unknown;
|
|
116
|
+
response: {
|
|
117
|
+
200: Response | import("..").VoiceTelephonyWebhookDecision<TResult>;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
}, {
|
|
122
|
+
derive: {};
|
|
123
|
+
resolve: {};
|
|
124
|
+
schema: {};
|
|
125
|
+
standaloneSchema: {};
|
|
126
|
+
response: {};
|
|
127
|
+
}, {
|
|
128
|
+
derive: {};
|
|
129
|
+
resolve: {};
|
|
130
|
+
schema: {};
|
|
131
|
+
standaloneSchema: {};
|
|
132
|
+
response: {};
|
|
133
|
+
} & {
|
|
134
|
+
derive: {};
|
|
135
|
+
resolve: {};
|
|
136
|
+
schema: {};
|
|
137
|
+
standaloneSchema: {};
|
|
138
|
+
response: {};
|
|
139
|
+
}>;
|