@openclaw/voice-call 2026.1.29 → 2026.2.1

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +13 -9
  3. package/index.ts +45 -49
  4. package/openclaw.plugin.json +11 -53
  5. package/package.json +6 -3
  6. package/src/cli.ts +80 -113
  7. package/src/config.test.ts +1 -4
  8. package/src/config.ts +88 -110
  9. package/src/core-bridge.ts +14 -12
  10. package/src/manager/context.ts +1 -1
  11. package/src/manager/events.ts +18 -9
  12. package/src/manager/lookup.ts +3 -1
  13. package/src/manager/outbound.ts +46 -19
  14. package/src/manager/state.ts +4 -6
  15. package/src/manager/store.ts +6 -3
  16. package/src/manager/timers.ts +11 -8
  17. package/src/manager.test.ts +7 -10
  18. package/src/manager.ts +53 -75
  19. package/src/media-stream.test.ts +0 -1
  20. package/src/media-stream.ts +12 -26
  21. package/src/providers/mock.ts +13 -16
  22. package/src/providers/plivo.test.ts +0 -1
  23. package/src/providers/plivo.ts +27 -29
  24. package/src/providers/stt-openai-realtime.ts +8 -8
  25. package/src/providers/telnyx.ts +5 -11
  26. package/src/providers/tts-openai.ts +9 -14
  27. package/src/providers/twilio/api.ts +9 -12
  28. package/src/providers/twilio/webhook.ts +2 -4
  29. package/src/providers/twilio.test.ts +1 -5
  30. package/src/providers/twilio.ts +34 -46
  31. package/src/response-generator.ts +7 -20
  32. package/src/runtime.ts +12 -25
  33. package/src/telephony-audio.ts +14 -12
  34. package/src/telephony-tts.ts +21 -12
  35. package/src/tunnel.ts +7 -24
  36. package/src/types.ts +0 -1
  37. package/src/utils.ts +3 -1
  38. package/src/voice-mapping.ts +3 -1
  39. package/src/webhook-security.test.ts +12 -21
  40. package/src/webhook-security.ts +25 -29
  41. package/src/webhook.ts +22 -57
@@ -9,9 +9,7 @@
9
9
 
10
10
  import type { IncomingMessage } from "node:http";
11
11
  import type { Duplex } from "node:stream";
12
-
13
12
  import { WebSocket, WebSocketServer } from "ws";
14
-
15
13
  import type {
16
14
  OpenAIRealtimeSTTProvider,
17
15
  RealtimeSTTSession,
@@ -87,10 +85,7 @@ export class MediaStreamHandler {
87
85
  /**
88
86
  * Handle new WebSocket connection from Twilio.
89
87
  */
90
- private async handleConnection(
91
- ws: WebSocket,
92
- _request: IncomingMessage,
93
- ): Promise<void> {
88
+ private async handleConnection(ws: WebSocket, _request: IncomingMessage): Promise<void> {
94
89
  let session: StreamSession | null = null;
95
90
 
96
91
  ws.on("message", async (data: Buffer) => {
@@ -140,16 +135,11 @@ export class MediaStreamHandler {
140
135
  /**
141
136
  * Handle stream start event.
142
137
  */
143
- private async handleStart(
144
- ws: WebSocket,
145
- message: TwilioMediaMessage,
146
- ): Promise<StreamSession> {
138
+ private async handleStart(ws: WebSocket, message: TwilioMediaMessage): Promise<StreamSession> {
147
139
  const streamSid = message.streamSid || "";
148
140
  const callSid = message.start?.callSid || "";
149
141
 
150
- console.log(
151
- `[MediaStream] Stream started: ${streamSid} (call: ${callSid})`,
152
- );
142
+ console.log(`[MediaStream] Stream started: ${streamSid} (call: ${callSid})`);
153
143
 
154
144
  // Create STT session
155
145
  const sttSession = this.config.sttProvider.createSession();
@@ -181,10 +171,7 @@ export class MediaStreamHandler {
181
171
 
182
172
  // Connect to OpenAI STT (non-blocking, log errors but don't fail the call)
183
173
  sttSession.connect().catch((err) => {
184
- console.warn(
185
- `[MediaStream] STT connection failed (TTS still works):`,
186
- err.message,
187
- );
174
+ console.warn(`[MediaStream] STT connection failed (TTS still works):`, err.message);
188
175
  });
189
176
 
190
177
  return session;
@@ -252,10 +239,7 @@ export class MediaStreamHandler {
252
239
  * Queue a TTS operation for sequential playback.
253
240
  * Only one TTS operation plays at a time per stream to prevent overlap.
254
241
  */
255
- async queueTts(
256
- streamSid: string,
257
- playFn: (signal: AbortSignal) => Promise<void>,
258
- ): Promise<void> {
242
+ async queueTts(streamSid: string, playFn: (signal: AbortSignal) => Promise<void>): Promise<void> {
259
243
  const queue = this.getTtsQueue(streamSid);
260
244
  let resolveEntry: () => void;
261
245
  let rejectEntry: (error: unknown) => void;
@@ -292,9 +276,7 @@ export class MediaStreamHandler {
292
276
  * Get active session by call ID.
293
277
  */
294
278
  getSessionByCallId(callId: string): StreamSession | undefined {
295
- return [...this.sessions.values()].find(
296
- (session) => session.callId === callId,
297
- );
279
+ return [...this.sessions.values()].find((session) => session.callId === callId);
298
280
  }
299
281
 
300
282
  /**
@@ -311,7 +293,9 @@ export class MediaStreamHandler {
311
293
 
312
294
  private getTtsQueue(streamSid: string): TtsQueueEntry[] {
313
295
  const existing = this.ttsQueues.get(streamSid);
314
- if (existing) return existing;
296
+ if (existing) {
297
+ return existing;
298
+ }
315
299
  const queue: TtsQueueEntry[] = [];
316
300
  this.ttsQueues.set(streamSid, queue);
317
301
  return queue;
@@ -355,7 +339,9 @@ export class MediaStreamHandler {
355
339
 
356
340
  private clearTtsState(streamSid: string): void {
357
341
  const queue = this.ttsQueues.get(streamSid);
358
- if (queue) queue.length = 0;
342
+ if (queue) {
343
+ queue.length = 0;
344
+ }
359
345
  this.ttsActiveControllers.get(streamSid)?.abort();
360
346
  this.ttsActiveControllers.delete(streamSid);
361
347
  this.ttsPlaying.delete(streamSid);
@@ -1,5 +1,4 @@
1
1
  import crypto from "node:crypto";
2
-
3
2
  import type {
4
3
  EndReason,
5
4
  HangupCallInput,
@@ -37,11 +36,15 @@ export class MockProvider implements VoiceCallProvider {
37
36
  if (Array.isArray(payload.events)) {
38
37
  for (const evt of payload.events) {
39
38
  const normalized = this.normalizeEvent(evt);
40
- if (normalized) events.push(normalized);
39
+ if (normalized) {
40
+ events.push(normalized);
41
+ }
41
42
  }
42
43
  } else if (payload.event) {
43
44
  const normalized = this.normalizeEvent(payload.event);
44
- if (normalized) events.push(normalized);
45
+ if (normalized) {
46
+ events.push(normalized);
47
+ }
45
48
  }
46
49
 
47
50
  return { events, statusCode: 200 };
@@ -50,10 +53,10 @@ export class MockProvider implements VoiceCallProvider {
50
53
  }
51
54
  }
52
55
 
53
- private normalizeEvent(
54
- evt: Partial<NormalizedEvent>,
55
- ): NormalizedEvent | null {
56
- if (!evt.type || !evt.callId) return null;
56
+ private normalizeEvent(evt: Partial<NormalizedEvent>): NormalizedEvent | null {
57
+ if (!evt.type || !evt.callId) {
58
+ return null;
59
+ }
57
60
 
58
61
  const base = {
59
62
  id: evt.id || crypto.randomUUID(),
@@ -96,9 +99,7 @@ export class MockProvider implements VoiceCallProvider {
96
99
  }
97
100
 
98
101
  case "call.silence": {
99
- const payload = evt as Partial<
100
- NormalizedEvent & { durationMs?: number }
101
- >;
102
+ const payload = evt as Partial<NormalizedEvent & { durationMs?: number }>;
102
103
  return {
103
104
  ...base,
104
105
  type: evt.type,
@@ -116,9 +117,7 @@ export class MockProvider implements VoiceCallProvider {
116
117
  }
117
118
 
118
119
  case "call.ended": {
119
- const payload = evt as Partial<
120
- NormalizedEvent & { reason?: EndReason }
121
- >;
120
+ const payload = evt as Partial<NormalizedEvent & { reason?: EndReason }>;
122
121
  return {
123
122
  ...base,
124
123
  type: evt.type,
@@ -127,9 +126,7 @@ export class MockProvider implements VoiceCallProvider {
127
126
  }
128
127
 
129
128
  case "call.error": {
130
- const payload = evt as Partial<
131
- NormalizedEvent & { error?: string; retryable?: boolean }
132
- >;
129
+ const payload = evt as Partial<NormalizedEvent & { error?: string; retryable?: boolean }>;
133
130
  return {
134
131
  ...base,
135
132
  type: evt.type,
@@ -1,5 +1,4 @@
1
1
  import { describe, expect, it } from "vitest";
2
-
3
2
  import { PlivoProvider } from "./plivo.js";
4
3
 
5
4
  describe("PlivoProvider", () => {
@@ -1,5 +1,4 @@
1
1
  import crypto from "node:crypto";
2
-
3
2
  import type { PlivoConfig } from "../config.js";
4
3
  import type {
5
4
  HangupCallInput,
@@ -13,9 +12,9 @@ import type {
13
12
  WebhookContext,
14
13
  WebhookVerificationResult,
15
14
  } from "../types.js";
15
+ import type { VoiceCallProvider } from "./base.js";
16
16
  import { escapeXml } from "../voice-mapping.js";
17
17
  import { reconstructWebhookUrl, verifyPlivoWebhook } from "../webhook-security.js";
18
- import type { VoiceCallProvider } from "./base.js";
19
18
 
20
19
  export interface PlivoProviderOptions {
21
20
  /** Override public URL origin for signature verification */
@@ -103,8 +102,7 @@ export class PlivoProvider implements VoiceCallProvider {
103
102
  }
104
103
 
105
104
  parseWebhookEvent(ctx: WebhookContext): ProviderWebhookParseResult {
106
- const flow =
107
- typeof ctx.query?.flow === "string" ? ctx.query.flow.trim() : "";
105
+ const flow = typeof ctx.query?.flow === "string" ? ctx.query.flow.trim() : "";
108
106
 
109
107
  const parsed = this.parseBody(ctx.rawBody);
110
108
  if (!parsed) {
@@ -124,7 +122,9 @@ export class PlivoProvider implements VoiceCallProvider {
124
122
  if (flow === "xml-speak") {
125
123
  const callId = this.getCallIdFromQuery(ctx);
126
124
  const pending = callId ? this.pendingSpeakByCallId.get(callId) : undefined;
127
- if (callId) this.pendingSpeakByCallId.delete(callId);
125
+ if (callId) {
126
+ this.pendingSpeakByCallId.delete(callId);
127
+ }
128
128
 
129
129
  const xml = pending
130
130
  ? PlivoProvider.xmlSpeak(pending.text, pending.locale)
@@ -139,10 +139,10 @@ export class PlivoProvider implements VoiceCallProvider {
139
139
 
140
140
  if (flow === "xml-listen") {
141
141
  const callId = this.getCallIdFromQuery(ctx);
142
- const pending = callId
143
- ? this.pendingListenByCallId.get(callId)
144
- : undefined;
145
- if (callId) this.pendingListenByCallId.delete(callId);
142
+ const pending = callId ? this.pendingListenByCallId.get(callId) : undefined;
143
+ if (callId) {
144
+ this.pendingListenByCallId.delete(callId);
145
+ }
146
146
 
147
147
  const actionUrl = this.buildActionUrl(ctx, {
148
148
  flow: "getinput",
@@ -180,10 +180,7 @@ export class PlivoProvider implements VoiceCallProvider {
180
180
  };
181
181
  }
182
182
 
183
- private normalizeEvent(
184
- params: URLSearchParams,
185
- callIdOverride?: string,
186
- ): NormalizedEvent | null {
183
+ private normalizeEvent(params: URLSearchParams, callIdOverride?: string): NormalizedEvent | null {
187
184
  const callUuid = params.get("CallUUID") || "";
188
185
  const requestUuid = params.get("RequestUUID") || "";
189
186
 
@@ -329,11 +326,9 @@ export class PlivoProvider implements VoiceCallProvider {
329
326
  }
330
327
 
331
328
  async playTts(input: PlayTtsInput): Promise<void> {
332
- const callUuid = this.requestUuidToCallUuid.get(input.providerCallId) ??
333
- input.providerCallId;
329
+ const callUuid = this.requestUuidToCallUuid.get(input.providerCallId) ?? input.providerCallId;
334
330
  const webhookBase =
335
- this.callUuidToWebhookUrl.get(callUuid) ||
336
- this.callIdToWebhookUrl.get(input.callId);
331
+ this.callUuidToWebhookUrl.get(callUuid) || this.callIdToWebhookUrl.get(input.callId);
337
332
  if (!webhookBase) {
338
333
  throw new Error("Missing webhook URL for this call (provider state missing)");
339
334
  }
@@ -364,11 +359,9 @@ export class PlivoProvider implements VoiceCallProvider {
364
359
  }
365
360
 
366
361
  async startListening(input: StartListeningInput): Promise<void> {
367
- const callUuid = this.requestUuidToCallUuid.get(input.providerCallId) ??
368
- input.providerCallId;
362
+ const callUuid = this.requestUuidToCallUuid.get(input.providerCallId) ?? input.providerCallId;
369
363
  const webhookBase =
370
- this.callUuidToWebhookUrl.get(callUuid) ||
371
- this.callIdToWebhookUrl.get(input.callId);
364
+ this.callUuidToWebhookUrl.get(callUuid) || this.callIdToWebhookUrl.get(input.callId);
372
365
  if (!webhookBase) {
373
366
  throw new Error("Missing webhook URL for this call (provider state missing)");
374
367
  }
@@ -403,7 +396,9 @@ export class PlivoProvider implements VoiceCallProvider {
403
396
 
404
397
  private static normalizeNumber(numberOrSip: string): string {
405
398
  const trimmed = numberOrSip.trim();
406
- if (trimmed.toLowerCase().startsWith("sip:")) return trimmed;
399
+ if (trimmed.toLowerCase().startsWith("sip:")) {
400
+ return trimmed;
401
+ }
407
402
  return trimmed.replace(/[^\d+]/g, "");
408
403
  }
409
404
 
@@ -427,10 +422,7 @@ export class PlivoProvider implements VoiceCallProvider {
427
422
  </Response>`;
428
423
  }
429
424
 
430
- private static xmlGetInputSpeech(params: {
431
- actionUrl: string;
432
- language?: string;
433
- }): string {
425
+ private static xmlGetInputSpeech(params: { actionUrl: string; language?: string }): string {
434
426
  const language = params.language || "en-US";
435
427
  return `<?xml version="1.0" encoding="UTF-8"?>
436
428
  <Response>
@@ -453,12 +445,16 @@ export class PlivoProvider implements VoiceCallProvider {
453
445
  opts: { flow: string; callId?: string },
454
446
  ): string | null {
455
447
  const base = PlivoProvider.baseWebhookUrlFromCtx(ctx);
456
- if (!base) return null;
448
+ if (!base) {
449
+ return null;
450
+ }
457
451
 
458
452
  const u = new URL(base);
459
453
  u.searchParams.set("provider", "plivo");
460
454
  u.searchParams.set("flow", opts.flow);
461
- if (opts.callId) u.searchParams.set("callId", opts.callId);
455
+ if (opts.callId) {
456
+ u.searchParams.set("callId", opts.callId);
457
+ }
462
458
  return u.toString();
463
459
  }
464
460
 
@@ -491,7 +487,9 @@ export class PlivoProvider implements VoiceCallProvider {
491
487
 
492
488
  for (const key of candidates) {
493
489
  const value = params.get(key);
494
- if (value && value.trim()) return value.trim();
490
+ if (value && value.trim()) {
491
+ return value.trim();
492
+ }
495
493
  }
496
494
  return null;
497
495
  }
@@ -155,7 +155,9 @@ class OpenAIRealtimeSTTSession implements RealtimeSTTSession {
155
155
 
156
156
  this.ws.on("error", (error) => {
157
157
  console.error("[RealtimeSTT] WebSocket error:", error);
158
- if (!this.connected) reject(error);
158
+ if (!this.connected) {
159
+ reject(error);
160
+ }
159
161
  });
160
162
 
161
163
  this.ws.on("close", (code, reason) => {
@@ -183,9 +185,7 @@ class OpenAIRealtimeSTTSession implements RealtimeSTTSession {
183
185
  return;
184
186
  }
185
187
 
186
- if (
187
- this.reconnectAttempts >= OpenAIRealtimeSTTSession.MAX_RECONNECT_ATTEMPTS
188
- ) {
188
+ if (this.reconnectAttempts >= OpenAIRealtimeSTTSession.MAX_RECONNECT_ATTEMPTS) {
189
189
  console.error(
190
190
  `[RealtimeSTT] Max reconnect attempts (${OpenAIRealtimeSTTSession.MAX_RECONNECT_ATTEMPTS}) reached`,
191
191
  );
@@ -193,9 +193,7 @@ class OpenAIRealtimeSTTSession implements RealtimeSTTSession {
193
193
  }
194
194
 
195
195
  this.reconnectAttempts++;
196
- const delay =
197
- OpenAIRealtimeSTTSession.RECONNECT_DELAY_MS *
198
- 2 ** (this.reconnectAttempts - 1);
196
+ const delay = OpenAIRealtimeSTTSession.RECONNECT_DELAY_MS * 2 ** (this.reconnectAttempts - 1);
199
197
  console.log(
200
198
  `[RealtimeSTT] Reconnecting ${this.reconnectAttempts}/${OpenAIRealtimeSTTSession.MAX_RECONNECT_ATTEMPTS} in ${delay}ms...`,
201
199
  );
@@ -262,7 +260,9 @@ class OpenAIRealtimeSTTSession implements RealtimeSTTSession {
262
260
  }
263
261
 
264
262
  sendAudio(muLawData: Buffer): void {
265
- if (!this.connected) return;
263
+ if (!this.connected) {
264
+ return;
265
+ }
266
266
  this.sendEvent({
267
267
  type: "input_audio_buffer.append",
268
268
  audio: muLawData.toString("base64"),
@@ -1,5 +1,4 @@
1
1
  import crypto from "node:crypto";
2
-
3
2
  import type { TelnyxConfig } from "../config.js";
4
3
  import type {
5
4
  EndReason,
@@ -161,9 +160,7 @@ export class TelnyxProvider implements VoiceCallProvider {
161
160
  let callId = "";
162
161
  if (data.payload?.client_state) {
163
162
  try {
164
- callId = Buffer.from(data.payload.client_state, "base64").toString(
165
- "utf8",
166
- );
163
+ callId = Buffer.from(data.payload.client_state, "base64").toString("utf8");
167
164
  } catch {
168
165
  // Fallback if not valid Base64
169
166
  callId = data.payload.client_state;
@@ -312,13 +309,10 @@ export class TelnyxProvider implements VoiceCallProvider {
312
309
  * Start transcription (STT) via Telnyx.
313
310
  */
314
311
  async startListening(input: StartListeningInput): Promise<void> {
315
- await this.apiRequest(
316
- `/calls/${input.providerCallId}/actions/transcription_start`,
317
- {
318
- command_id: crypto.randomUUID(),
319
- language: input.language || "en",
320
- },
321
- );
312
+ await this.apiRequest(`/calls/${input.providerCallId}/actions/transcription_start`, {
313
+ command_id: crypto.randomUUID(),
314
+ language: input.language || "en",
315
+ });
322
316
  }
323
317
 
324
318
  /**
@@ -84,9 +84,7 @@ export class OpenAITTSProvider {
84
84
  this.instructions = config.instructions;
85
85
 
86
86
  if (!this.apiKey) {
87
- throw new Error(
88
- "OpenAI API key required (set OPENAI_API_KEY or pass apiKey)",
89
- );
87
+ throw new Error("OpenAI API key required (set OPENAI_API_KEY or pass apiKey)");
90
88
  }
91
89
  }
92
90
 
@@ -207,19 +205,19 @@ function linearToMulaw(sample: number): number {
207
205
 
208
206
  // Get sign bit
209
207
  const sign = sample < 0 ? 0x80 : 0;
210
- if (sample < 0) sample = -sample;
208
+ if (sample < 0) {
209
+ sample = -sample;
210
+ }
211
211
 
212
212
  // Clip to prevent overflow
213
- if (sample > CLIP) sample = CLIP;
213
+ if (sample > CLIP) {
214
+ sample = CLIP;
215
+ }
214
216
 
215
217
  // Add bias and find segment
216
218
  sample += BIAS;
217
219
  let exponent = 7;
218
- for (
219
- let expMask = 0x4000;
220
- (sample & expMask) === 0 && exponent > 0;
221
- exponent--, expMask >>= 1
222
- ) {
220
+ for (let expMask = 0x4000; (sample & expMask) === 0 && exponent > 0; exponent--, expMask >>= 1) {
223
221
  // Find the segment (exponent)
224
222
  }
225
223
 
@@ -252,10 +250,7 @@ export function mulawToLinear(mulaw: number): number {
252
250
  * Chunk audio buffer into 20ms frames for streaming.
253
251
  * At 8kHz mono, 20ms = 160 samples = 160 bytes (mu-law).
254
252
  */
255
- export function chunkAudio(
256
- audio: Buffer,
257
- chunkSize = 160,
258
- ): Generator<Buffer, void, unknown> {
253
+ export function chunkAudio(audio: Buffer, chunkSize = 160): Generator<Buffer, void, unknown> {
259
254
  return (function* () {
260
255
  for (let i = 0; i < audio.length; i += chunkSize) {
261
256
  yield audio.subarray(i, Math.min(i + chunkSize, audio.length));
@@ -9,19 +9,16 @@ export async function twilioApiRequest<T = unknown>(params: {
9
9
  const bodyParams =
10
10
  params.body instanceof URLSearchParams
11
11
  ? params.body
12
- : Object.entries(params.body).reduce<URLSearchParams>(
13
- (acc, [key, value]) => {
14
- if (Array.isArray(value)) {
15
- for (const entry of value) {
16
- acc.append(key, entry);
17
- }
18
- } else if (typeof value === "string") {
19
- acc.append(key, value);
12
+ : Object.entries(params.body).reduce<URLSearchParams>((acc, [key, value]) => {
13
+ if (Array.isArray(value)) {
14
+ for (const entry of value) {
15
+ acc.append(key, entry);
20
16
  }
21
- return acc;
22
- },
23
- new URLSearchParams(),
24
- );
17
+ } else if (typeof value === "string") {
18
+ acc.append(key, value);
19
+ }
20
+ return acc;
21
+ }, new URLSearchParams());
25
22
 
26
23
  const response = await fetch(`${params.baseUrl}${params.endpoint}`, {
27
24
  method: "POST",
@@ -1,7 +1,6 @@
1
1
  import type { WebhookContext, WebhookVerificationResult } from "../../types.js";
2
- import { verifyTwilioWebhook } from "../../webhook-security.js";
3
-
4
2
  import type { TwilioProviderOptions } from "../twilio.js";
3
+ import { verifyTwilioWebhook } from "../../webhook-security.js";
5
4
 
6
5
  export function verifyTwilioProviderWebhook(params: {
7
6
  ctx: WebhookContext;
@@ -11,8 +10,7 @@ export function verifyTwilioProviderWebhook(params: {
11
10
  }): WebhookVerificationResult {
12
11
  const result = verifyTwilioWebhook(params.ctx, params.authToken, {
13
12
  publicUrl: params.currentPublicUrl || undefined,
14
- allowNgrokFreeTierLoopbackBypass:
15
- params.options.allowNgrokFreeTierLoopbackBypass ?? false,
13
+ allowNgrokFreeTierLoopbackBypass: params.options.allowNgrokFreeTierLoopbackBypass ?? false,
16
14
  skipVerification: params.options.skipVerification,
17
15
  });
18
16
 
@@ -1,5 +1,4 @@
1
1
  import { describe, expect, it } from "vitest";
2
-
3
2
  import type { WebhookContext } from "../types.js";
4
3
  import { TwilioProvider } from "./twilio.js";
5
4
 
@@ -12,10 +11,7 @@ function createProvider(): TwilioProvider {
12
11
  );
13
12
  }
14
13
 
15
- function createContext(
16
- rawBody: string,
17
- query?: WebhookContext["query"],
18
- ): WebhookContext {
14
+ function createContext(rawBody: string, query?: WebhookContext["query"]): WebhookContext {
19
15
  return {
20
16
  headers: {},
21
17
  rawBody,