@absolutejs/voice 0.0.22-beta.75 → 0.0.22-beta.77

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 CHANGED
@@ -81,7 +81,9 @@ export type { VoiceS3ReviewStoreClient, VoiceS3ReviewStoreFile, VoiceS3ReviewSto
81
81
  export type { VoiceSQLiteRuntimeStorage, VoiceSQLiteStoreOptions } from './sqliteStore';
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
- export type { TwilioInboundMessage, TwilioMediaStreamBridge, TwilioMediaStreamBridgeOptions, TwilioMediaStreamSocket, TwilioOutboundClearMessage, TwilioOutboundMarkMessage, TwilioOutboundMediaMessage, TwilioOutboundMessage, TwilioVoiceRouteParameters, TwilioVoiceResponseOptions, TwilioVoiceSetupOptions, TwilioVoiceSetupStatus, TwilioVoiceRoutesOptions } from './telephony/twilio';
84
+ export { evaluateVoiceTelephonyContract } from './telephony/contract';
85
+ export type { TwilioInboundMessage, TwilioMediaStreamBridge, TwilioMediaStreamBridgeOptions, TwilioMediaStreamSocket, TwilioOutboundClearMessage, TwilioOutboundMarkMessage, TwilioOutboundMediaMessage, TwilioOutboundMessage, TwilioVoiceRouteParameters, TwilioVoiceResponseOptions, TwilioVoiceSmokeCheck, TwilioVoiceSmokeOptions, TwilioVoiceSmokeReport, TwilioVoiceSetupOptions, TwilioVoiceSetupStatus, TwilioVoiceRoutesOptions } from './telephony/twilio';
86
+ export type { VoiceTelephonyContractIssue, VoiceTelephonyContractOptions, VoiceTelephonyContractReport, VoiceTelephonyContractRequirement, VoiceTelephonyProvider, VoiceTelephonySetupStatus, VoiceTelephonySmokeCheck, VoiceTelephonySmokeReport } from './telephony/contract';
85
87
  export { shapeTelephonyAssistantText } from './telephony/response';
86
88
  export type { TelephonyResponseShapeMode, TelephonyResponseShapeOptions } from './telephony/response';
87
89
  export * from './types';
package/dist/index.js CHANGED
@@ -15288,6 +15288,104 @@ ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml17(st
15288
15288
  ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml17(name)}</code></li>`).join("")}</ul></section>` : ""}
15289
15289
  ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml17(warning)}</li>`).join("")}</ul></section>` : ""}
15290
15290
  </main>`;
15291
+ var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&amp;", "&");
15292
+ var createSmokeCheck = (name, status, message, details) => ({
15293
+ details,
15294
+ message,
15295
+ name,
15296
+ status
15297
+ });
15298
+ var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
15299
+ <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
15300
+ <h1>${escapeHtml17(title)}</h1>
15301
+ <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
15302
+ <section>
15303
+ <h2>Checks</h2>
15304
+ <ul>
15305
+ ${report.checks.map((check) => `<li><strong>${escapeHtml17(check.name)}</strong>: ${escapeHtml17(check.status)}${check.message ? ` - ${escapeHtml17(check.message)}` : ""}</li>`).join("")}
15306
+ </ul>
15307
+ </section>
15308
+ <section>
15309
+ <h2>Observed URLs</h2>
15310
+ <ul>
15311
+ <li><strong>TwiML:</strong> <code>${escapeHtml17(report.setup.urls.twiml)}</code></li>
15312
+ <li><strong>Stream:</strong> <code>${escapeHtml17(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
15313
+ <li><strong>Webhook:</strong> <code>${escapeHtml17(report.setup.urls.webhook)}</code></li>
15314
+ </ul>
15315
+ </section>
15316
+ </main>`;
15317
+ var runTwilioVoiceSmokeTest = async (input) => {
15318
+ const setup = await buildTwilioVoiceSetupStatus(input.options, input);
15319
+ const checks = [];
15320
+ const twimlUrl = new URL(setup.urls.twiml);
15321
+ twimlUrl.searchParams.set("scenarioId", input.options.smoke?.scenarioId ?? "smoke");
15322
+ twimlUrl.searchParams.set("sessionId", input.options.smoke?.sessionId ?? "smoke-session");
15323
+ const twimlResponse = await input.app.handle(new Request(twimlUrl, {
15324
+ headers: input.request.headers
15325
+ }));
15326
+ const twiml = await twimlResponse.text();
15327
+ const streamUrl = extractTwilioStreamUrl(twiml);
15328
+ checks.push(createSmokeCheck("twiml", twimlResponse.ok && Boolean(streamUrl) ? "pass" : "fail", streamUrl ? "TwiML includes a media stream URL." : 'TwiML is missing <Stream url="...">.', {
15329
+ status: twimlResponse.status,
15330
+ streamUrl
15331
+ }));
15332
+ checks.push(createSmokeCheck("stream-url", streamUrl?.startsWith("wss://") ? "pass" : "fail", streamUrl?.startsWith("wss://") ? "Media stream URL uses wss://." : "Media stream URL should use wss:// for Twilio.", {
15333
+ streamUrl
15334
+ }));
15335
+ const webhookBody = {
15336
+ CallSid: input.options.smoke?.callSid ?? "CA_SMOKE_TEST",
15337
+ CallStatus: input.options.smoke?.status ?? "busy",
15338
+ SipResponseCode: String(input.options.smoke?.sipCode ?? 486)
15339
+ };
15340
+ const webhookHeaders = new Headers({
15341
+ "content-type": "application/x-www-form-urlencoded"
15342
+ });
15343
+ const verificationUrl = setup.signing.verificationUrl ?? setup.urls.webhook;
15344
+ if (input.options.webhook?.signingSecret) {
15345
+ webhookHeaders.set("x-twilio-signature", await signVoiceTwilioWebhook({
15346
+ authToken: input.options.webhook.signingSecret,
15347
+ body: webhookBody,
15348
+ url: verificationUrl
15349
+ }));
15350
+ }
15351
+ const webhookResponse = await input.app.handle(new Request(setup.urls.webhook, {
15352
+ body: new URLSearchParams(webhookBody),
15353
+ headers: webhookHeaders,
15354
+ method: "POST"
15355
+ }));
15356
+ const webhookText = await webhookResponse.text();
15357
+ const webhookPayload = (() => {
15358
+ try {
15359
+ return JSON.parse(webhookText);
15360
+ } catch {
15361
+ return webhookText;
15362
+ }
15363
+ })();
15364
+ checks.push(createSmokeCheck("webhook", webhookResponse.ok ? "pass" : "fail", webhookResponse.ok ? "Synthetic Twilio status callback was accepted." : "Synthetic Twilio status callback failed.", {
15365
+ status: webhookResponse.status
15366
+ }));
15367
+ for (const warning of setup.warnings) {
15368
+ checks.push(createSmokeCheck("setup-warning", "warn", warning));
15369
+ }
15370
+ for (const name of setup.missing) {
15371
+ checks.push(createSmokeCheck("missing-env", "fail", `${name} is missing.`));
15372
+ }
15373
+ return {
15374
+ checks,
15375
+ generatedAt: Date.now(),
15376
+ pass: checks.every((check) => check.status !== "fail"),
15377
+ provider: "twilio",
15378
+ setup,
15379
+ twiml: {
15380
+ status: twimlResponse.status,
15381
+ streamUrl
15382
+ },
15383
+ webhook: {
15384
+ body: webhookPayload,
15385
+ status: webhookResponse.status
15386
+ }
15387
+ };
15388
+ };
15291
15389
  var normalizeOnTurn2 = (handler) => {
15292
15390
  if (handler.length > 1) {
15293
15391
  const directHandler = handler;
@@ -15669,6 +15767,7 @@ var createTwilioVoiceRoutes = (options) => {
15669
15767
  const twimlPath = options.twiml?.path ?? "/api/voice/twilio";
15670
15768
  const webhookPath = options.webhook?.path ?? "/api/voice/twilio/webhook";
15671
15769
  const setupPath = options.setup?.path === false ? false : options.setup?.path ?? "/api/voice/twilio/setup";
15770
+ const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
15672
15771
  const bridges = new WeakMap;
15673
15772
  const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
15674
15773
  const app = new Elysia18({
@@ -15742,9 +15841,30 @@ var createTwilioVoiceRoutes = (options) => {
15742
15841
  provider: "twilio"
15743
15842
  }));
15744
15843
  if (!setupPath) {
15745
- return app;
15844
+ if (!smokePath) {
15845
+ return app;
15846
+ }
15847
+ return app.get(smokePath, async ({ query, request }) => {
15848
+ const report = await runTwilioVoiceSmokeTest({
15849
+ app,
15850
+ options,
15851
+ query,
15852
+ request,
15853
+ streamPath,
15854
+ twimlPath,
15855
+ webhookPath
15856
+ });
15857
+ if (query.format === "html") {
15858
+ return new Response(renderTwilioVoiceSmokeHTML(report, options.smoke?.title ?? "AbsoluteJS Twilio Voice Smoke Test"), {
15859
+ headers: {
15860
+ "content-type": "text/html; charset=utf-8"
15861
+ }
15862
+ });
15863
+ }
15864
+ return report;
15865
+ });
15746
15866
  }
15747
- return app.get(setupPath, async ({ query, request }) => {
15867
+ const withSetup = app.get(setupPath, async ({ query, request }) => {
15748
15868
  const status = await buildTwilioVoiceSetupStatus(options, {
15749
15869
  query,
15750
15870
  request,
@@ -15761,6 +15881,107 @@ var createTwilioVoiceRoutes = (options) => {
15761
15881
  }
15762
15882
  return status;
15763
15883
  });
15884
+ if (!smokePath) {
15885
+ return withSetup;
15886
+ }
15887
+ return withSetup.get(smokePath, async ({ query, request }) => {
15888
+ const report = await runTwilioVoiceSmokeTest({
15889
+ app,
15890
+ options,
15891
+ query,
15892
+ request,
15893
+ streamPath,
15894
+ twimlPath,
15895
+ webhookPath
15896
+ });
15897
+ if (query.format === "html") {
15898
+ return new Response(renderTwilioVoiceSmokeHTML(report, options.smoke?.title ?? "AbsoluteJS Twilio Voice Smoke Test"), {
15899
+ headers: {
15900
+ "content-type": "text/html; charset=utf-8"
15901
+ }
15902
+ });
15903
+ }
15904
+ return report;
15905
+ });
15906
+ };
15907
+ // src/telephony/contract.ts
15908
+ var DEFAULT_REQUIREMENTS = [
15909
+ "stream-url",
15910
+ "wss-stream",
15911
+ "webhook-url",
15912
+ "signed-webhook",
15913
+ "smoke-pass"
15914
+ ];
15915
+ var hasFailingSmokeCheck = (smoke) => smoke?.checks.some((check) => check.status === "fail") ?? false;
15916
+ var evaluateVoiceTelephonyContract = (input) => {
15917
+ const requirements = input.options?.requirements ?? DEFAULT_REQUIREMENTS;
15918
+ const issues = [];
15919
+ const hasRequirement = (requirement) => requirements.includes(requirement);
15920
+ if (hasRequirement("stream-url") && !input.setup.urls.stream) {
15921
+ issues.push({
15922
+ message: "Missing media stream URL.",
15923
+ requirement: "stream-url",
15924
+ severity: "error"
15925
+ });
15926
+ }
15927
+ if (hasRequirement("wss-stream") && !input.setup.urls.stream.startsWith("wss://")) {
15928
+ issues.push({
15929
+ message: "Media stream URL must use wss://.",
15930
+ requirement: "wss-stream",
15931
+ severity: "error"
15932
+ });
15933
+ }
15934
+ if (hasRequirement("webhook-url") && !input.setup.urls.webhook) {
15935
+ issues.push({
15936
+ message: "Missing carrier webhook URL.",
15937
+ requirement: "webhook-url",
15938
+ severity: "error"
15939
+ });
15940
+ }
15941
+ if (hasRequirement("signed-webhook") && !input.setup.signing.configured) {
15942
+ issues.push({
15943
+ message: "Carrier webhook signature verification is not configured.",
15944
+ requirement: "signed-webhook",
15945
+ severity: "error"
15946
+ });
15947
+ }
15948
+ if (hasRequirement("smoke-pass")) {
15949
+ if (!input.smoke) {
15950
+ issues.push({
15951
+ message: "Missing telephony smoke test report.",
15952
+ requirement: "smoke-pass",
15953
+ severity: "error"
15954
+ });
15955
+ } else if (!input.smoke.pass || hasFailingSmokeCheck(input.smoke)) {
15956
+ issues.push({
15957
+ message: "Telephony smoke test did not pass.",
15958
+ requirement: "smoke-pass",
15959
+ severity: "error"
15960
+ });
15961
+ }
15962
+ }
15963
+ for (const warning of input.setup.warnings) {
15964
+ issues.push({
15965
+ message: warning,
15966
+ requirement: "stream-url",
15967
+ severity: "warning"
15968
+ });
15969
+ }
15970
+ for (const name of input.setup.missing) {
15971
+ issues.push({
15972
+ message: `${name} is missing.`,
15973
+ requirement: "webhook-url",
15974
+ severity: "error"
15975
+ });
15976
+ }
15977
+ return {
15978
+ issues,
15979
+ pass: issues.every((issue) => issue.severity !== "error"),
15980
+ provider: input.setup.provider,
15981
+ requirements,
15982
+ setup: input.setup,
15983
+ smoke: input.smoke
15984
+ };
15764
15985
  };
15765
15986
  // src/telephony/response.ts
15766
15987
  var normalizeWhitespace = (value) => value.replace(/\s+/g, " ").trim();
@@ -15904,6 +16125,7 @@ export {
15904
16125
  failVoiceOpsTask,
15905
16126
  exportVoiceTrace,
15906
16127
  evaluateVoiceTrace,
16128
+ evaluateVoiceTelephonyContract,
15907
16129
  evaluateVoiceQuality,
15908
16130
  encodeTwilioMulawBase64,
15909
16131
  deliverVoiceTraceEventsToSinks,
@@ -0,0 +1,61 @@
1
+ export type VoiceTelephonyProvider = 'generic' | 'plivo' | 'telnyx' | 'twilio';
2
+ export type VoiceTelephonySetupStatus<TProvider extends VoiceTelephonyProvider = VoiceTelephonyProvider> = {
3
+ generatedAt: number;
4
+ missing: string[];
5
+ provider: TProvider;
6
+ ready: boolean;
7
+ signing: {
8
+ configured: boolean;
9
+ mode: 'custom' | 'none' | 'provider-signature' | 'twilio-signature';
10
+ verificationUrl?: string;
11
+ };
12
+ urls: {
13
+ stream: string;
14
+ twiml?: string;
15
+ webhook: string;
16
+ };
17
+ warnings: string[];
18
+ };
19
+ export type VoiceTelephonySmokeCheck = {
20
+ details?: Record<string, unknown>;
21
+ message?: string;
22
+ name: string;
23
+ status: 'fail' | 'pass' | 'warn';
24
+ };
25
+ export type VoiceTelephonySmokeReport<TProvider extends VoiceTelephonyProvider = VoiceTelephonyProvider> = {
26
+ checks: VoiceTelephonySmokeCheck[];
27
+ generatedAt: number;
28
+ pass: boolean;
29
+ provider: TProvider;
30
+ setup: VoiceTelephonySetupStatus<TProvider>;
31
+ twiml?: {
32
+ status: number;
33
+ streamUrl?: string;
34
+ };
35
+ webhook?: {
36
+ body?: unknown;
37
+ status: number;
38
+ };
39
+ };
40
+ export type VoiceTelephonyContractRequirement = 'signed-webhook' | 'smoke-pass' | 'stream-url' | 'webhook-url' | 'wss-stream';
41
+ export type VoiceTelephonyContractIssue = {
42
+ requirement: VoiceTelephonyContractRequirement;
43
+ severity: 'error' | 'warning';
44
+ message: string;
45
+ };
46
+ export type VoiceTelephonyContractReport<TProvider extends VoiceTelephonyProvider = VoiceTelephonyProvider> = {
47
+ issues: VoiceTelephonyContractIssue[];
48
+ pass: boolean;
49
+ provider: TProvider;
50
+ requirements: VoiceTelephonyContractRequirement[];
51
+ setup: VoiceTelephonySetupStatus<TProvider>;
52
+ smoke?: VoiceTelephonySmokeReport<TProvider>;
53
+ };
54
+ export type VoiceTelephonyContractOptions = {
55
+ requirements?: VoiceTelephonyContractRequirement[];
56
+ };
57
+ export declare const evaluateVoiceTelephonyContract: <TProvider extends VoiceTelephonyProvider>(input: {
58
+ options?: VoiceTelephonyContractOptions;
59
+ setup: VoiceTelephonySetupStatus<TProvider>;
60
+ smoke?: VoiceTelephonySmokeReport<TProvider>;
61
+ }) => VoiceTelephonyContractReport<TProvider>;
@@ -1,4 +1,5 @@
1
1
  import { Elysia } from 'elysia';
2
+ import type { VoiceTelephonySetupStatus, VoiceTelephonySmokeCheck, VoiceTelephonySmokeReport } from './contract';
2
3
  import { type VoiceTelephonyOutcomePolicy, type VoiceTelephonyWebhookRoutesOptions } from '../telephonyOutcome';
3
4
  import { type VoiceCallReviewArtifact, type VoiceCallReviewConfig } from '../testing/review';
4
5
  import type { AudioFormat, VoiceLogger, VoicePluginConfig, VoiceSessionRecord, VoiceServerMessage } from '../types';
@@ -113,31 +114,33 @@ export type TwilioVoiceRouteParameters = Record<string, string | number | boolea
113
114
  query: Record<string, unknown>;
114
115
  request: Request;
115
116
  }) => Promise<Record<string, string | number | boolean | undefined>> | Record<string, string | number | boolean | undefined>);
116
- export type TwilioVoiceSetupStatus = {
117
- generatedAt: number;
118
- missing: string[];
119
- provider: 'twilio';
120
- ready: boolean;
121
- signing: {
122
- configured: boolean;
123
- mode: 'custom' | 'none' | 'twilio-signature';
124
- verificationUrl?: string;
125
- };
126
- urls: {
127
- stream: string;
117
+ export type TwilioVoiceSetupStatus = VoiceTelephonySetupStatus<'twilio'> & {
118
+ urls: VoiceTelephonySetupStatus<'twilio'>['urls'] & {
128
119
  twiml: string;
129
- webhook: string;
130
120
  };
131
- warnings: string[];
132
121
  };
133
122
  export type TwilioVoiceSetupOptions = {
134
123
  path?: false | string;
135
124
  requiredEnv?: Record<string, string | undefined>;
136
125
  title?: string;
137
126
  };
127
+ export type TwilioVoiceSmokeCheck = VoiceTelephonySmokeCheck;
128
+ export type TwilioVoiceSmokeReport = VoiceTelephonySmokeReport<'twilio'> & {
129
+ setup: TwilioVoiceSetupStatus;
130
+ };
131
+ export type TwilioVoiceSmokeOptions = {
132
+ callSid?: string;
133
+ path?: false | string;
134
+ scenarioId?: string;
135
+ sessionId?: string;
136
+ sipCode?: number;
137
+ status?: string;
138
+ title?: string;
139
+ };
138
140
  export type TwilioVoiceRoutesOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = TwilioMediaStreamBridgeOptions<TContext, TSession, TResult> & {
139
141
  name?: string;
140
142
  outcomePolicy?: VoiceTelephonyOutcomePolicy;
143
+ smoke?: TwilioVoiceSmokeOptions;
141
144
  setup?: TwilioVoiceSetupOptions;
142
145
  streamPath?: string;
143
146
  twiml?: {
@@ -8627,6 +8627,104 @@ ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml2(sta
8627
8627
  ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml2(name)}</code></li>`).join("")}</ul></section>` : ""}
8628
8628
  ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml2(warning)}</li>`).join("")}</ul></section>` : ""}
8629
8629
  </main>`;
8630
+ var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&amp;", "&");
8631
+ var createSmokeCheck = (name, status, message, details) => ({
8632
+ details,
8633
+ message,
8634
+ name,
8635
+ status
8636
+ });
8637
+ var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
8638
+ <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
8639
+ <h1>${escapeHtml2(title)}</h1>
8640
+ <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
8641
+ <section>
8642
+ <h2>Checks</h2>
8643
+ <ul>
8644
+ ${report.checks.map((check) => `<li><strong>${escapeHtml2(check.name)}</strong>: ${escapeHtml2(check.status)}${check.message ? ` - ${escapeHtml2(check.message)}` : ""}</li>`).join("")}
8645
+ </ul>
8646
+ </section>
8647
+ <section>
8648
+ <h2>Observed URLs</h2>
8649
+ <ul>
8650
+ <li><strong>TwiML:</strong> <code>${escapeHtml2(report.setup.urls.twiml)}</code></li>
8651
+ <li><strong>Stream:</strong> <code>${escapeHtml2(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
8652
+ <li><strong>Webhook:</strong> <code>${escapeHtml2(report.setup.urls.webhook)}</code></li>
8653
+ </ul>
8654
+ </section>
8655
+ </main>`;
8656
+ var runTwilioVoiceSmokeTest = async (input) => {
8657
+ const setup = await buildTwilioVoiceSetupStatus(input.options, input);
8658
+ const checks = [];
8659
+ const twimlUrl = new URL(setup.urls.twiml);
8660
+ twimlUrl.searchParams.set("scenarioId", input.options.smoke?.scenarioId ?? "smoke");
8661
+ twimlUrl.searchParams.set("sessionId", input.options.smoke?.sessionId ?? "smoke-session");
8662
+ const twimlResponse = await input.app.handle(new Request(twimlUrl, {
8663
+ headers: input.request.headers
8664
+ }));
8665
+ const twiml = await twimlResponse.text();
8666
+ const streamUrl = extractTwilioStreamUrl(twiml);
8667
+ checks.push(createSmokeCheck("twiml", twimlResponse.ok && Boolean(streamUrl) ? "pass" : "fail", streamUrl ? "TwiML includes a media stream URL." : 'TwiML is missing <Stream url="...">.', {
8668
+ status: twimlResponse.status,
8669
+ streamUrl
8670
+ }));
8671
+ checks.push(createSmokeCheck("stream-url", streamUrl?.startsWith("wss://") ? "pass" : "fail", streamUrl?.startsWith("wss://") ? "Media stream URL uses wss://." : "Media stream URL should use wss:// for Twilio.", {
8672
+ streamUrl
8673
+ }));
8674
+ const webhookBody = {
8675
+ CallSid: input.options.smoke?.callSid ?? "CA_SMOKE_TEST",
8676
+ CallStatus: input.options.smoke?.status ?? "busy",
8677
+ SipResponseCode: String(input.options.smoke?.sipCode ?? 486)
8678
+ };
8679
+ const webhookHeaders = new Headers({
8680
+ "content-type": "application/x-www-form-urlencoded"
8681
+ });
8682
+ const verificationUrl = setup.signing.verificationUrl ?? setup.urls.webhook;
8683
+ if (input.options.webhook?.signingSecret) {
8684
+ webhookHeaders.set("x-twilio-signature", await signVoiceTwilioWebhook({
8685
+ authToken: input.options.webhook.signingSecret,
8686
+ body: webhookBody,
8687
+ url: verificationUrl
8688
+ }));
8689
+ }
8690
+ const webhookResponse = await input.app.handle(new Request(setup.urls.webhook, {
8691
+ body: new URLSearchParams(webhookBody),
8692
+ headers: webhookHeaders,
8693
+ method: "POST"
8694
+ }));
8695
+ const webhookText = await webhookResponse.text();
8696
+ const webhookPayload = (() => {
8697
+ try {
8698
+ return JSON.parse(webhookText);
8699
+ } catch {
8700
+ return webhookText;
8701
+ }
8702
+ })();
8703
+ checks.push(createSmokeCheck("webhook", webhookResponse.ok ? "pass" : "fail", webhookResponse.ok ? "Synthetic Twilio status callback was accepted." : "Synthetic Twilio status callback failed.", {
8704
+ status: webhookResponse.status
8705
+ }));
8706
+ for (const warning of setup.warnings) {
8707
+ checks.push(createSmokeCheck("setup-warning", "warn", warning));
8708
+ }
8709
+ for (const name of setup.missing) {
8710
+ checks.push(createSmokeCheck("missing-env", "fail", `${name} is missing.`));
8711
+ }
8712
+ return {
8713
+ checks,
8714
+ generatedAt: Date.now(),
8715
+ pass: checks.every((check) => check.status !== "fail"),
8716
+ provider: "twilio",
8717
+ setup,
8718
+ twiml: {
8719
+ status: twimlResponse.status,
8720
+ streamUrl
8721
+ },
8722
+ webhook: {
8723
+ body: webhookPayload,
8724
+ status: webhookResponse.status
8725
+ }
8726
+ };
8727
+ };
8630
8728
  var normalizeOnTurn = (handler) => {
8631
8729
  if (handler.length > 1) {
8632
8730
  const directHandler = handler;
@@ -9008,6 +9106,7 @@ var createTwilioVoiceRoutes = (options) => {
9008
9106
  const twimlPath = options.twiml?.path ?? "/api/voice/twilio";
9009
9107
  const webhookPath = options.webhook?.path ?? "/api/voice/twilio/webhook";
9010
9108
  const setupPath = options.setup?.path === false ? false : options.setup?.path ?? "/api/voice/twilio/setup";
9109
+ const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
9011
9110
  const bridges = new WeakMap;
9012
9111
  const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
9013
9112
  const app = new Elysia2({
@@ -9081,9 +9180,30 @@ var createTwilioVoiceRoutes = (options) => {
9081
9180
  provider: "twilio"
9082
9181
  }));
9083
9182
  if (!setupPath) {
9084
- return app;
9183
+ if (!smokePath) {
9184
+ return app;
9185
+ }
9186
+ return app.get(smokePath, async ({ query, request }) => {
9187
+ const report = await runTwilioVoiceSmokeTest({
9188
+ app,
9189
+ options,
9190
+ query,
9191
+ request,
9192
+ streamPath,
9193
+ twimlPath,
9194
+ webhookPath
9195
+ });
9196
+ if (query.format === "html") {
9197
+ return new Response(renderTwilioVoiceSmokeHTML(report, options.smoke?.title ?? "AbsoluteJS Twilio Voice Smoke Test"), {
9198
+ headers: {
9199
+ "content-type": "text/html; charset=utf-8"
9200
+ }
9201
+ });
9202
+ }
9203
+ return report;
9204
+ });
9085
9205
  }
9086
- return app.get(setupPath, async ({ query, request }) => {
9206
+ const withSetup = app.get(setupPath, async ({ query, request }) => {
9087
9207
  const status = await buildTwilioVoiceSetupStatus(options, {
9088
9208
  query,
9089
9209
  request,
@@ -9100,6 +9220,28 @@ var createTwilioVoiceRoutes = (options) => {
9100
9220
  }
9101
9221
  return status;
9102
9222
  });
9223
+ if (!smokePath) {
9224
+ return withSetup;
9225
+ }
9226
+ return withSetup.get(smokePath, async ({ query, request }) => {
9227
+ const report = await runTwilioVoiceSmokeTest({
9228
+ app,
9229
+ options,
9230
+ query,
9231
+ request,
9232
+ streamPath,
9233
+ twimlPath,
9234
+ webhookPath
9235
+ });
9236
+ if (query.format === "html") {
9237
+ return new Response(renderTwilioVoiceSmokeHTML(report, options.smoke?.title ?? "AbsoluteJS Twilio Voice Smoke Test"), {
9238
+ headers: {
9239
+ "content-type": "text/html; charset=utf-8"
9240
+ }
9241
+ });
9242
+ }
9243
+ return report;
9244
+ });
9103
9245
  };
9104
9246
 
9105
9247
  // src/testing/telephony.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.75",
3
+ "version": "0.0.22-beta.77",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",