@better-webhook/cli 3.9.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/_binary_entry.js +29 -0
  2. package/dist/commands/capture.d.ts +2 -0
  3. package/dist/commands/capture.js +33 -0
  4. package/dist/commands/captures.d.ts +2 -0
  5. package/dist/commands/captures.js +316 -0
  6. package/dist/commands/dashboard.d.ts +2 -0
  7. package/dist/commands/dashboard.js +70 -0
  8. package/dist/commands/index.d.ts +6 -0
  9. package/dist/commands/index.js +6 -0
  10. package/dist/commands/replay.d.ts +2 -0
  11. package/dist/commands/replay.js +140 -0
  12. package/dist/commands/run.d.ts +2 -0
  13. package/dist/commands/run.js +182 -0
  14. package/dist/commands/templates.d.ts +2 -0
  15. package/dist/commands/templates.js +285 -0
  16. package/dist/core/capture-server.d.ts +37 -0
  17. package/dist/core/capture-server.js +400 -0
  18. package/dist/core/capture-server.test.d.ts +1 -0
  19. package/dist/core/capture-server.test.js +86 -0
  20. package/dist/core/cli-version.d.ts +1 -0
  21. package/dist/core/cli-version.js +30 -0
  22. package/dist/core/cli-version.test.d.ts +1 -0
  23. package/dist/core/cli-version.test.js +42 -0
  24. package/dist/core/dashboard-api.d.ts +8 -0
  25. package/dist/core/dashboard-api.js +333 -0
  26. package/dist/core/dashboard-server.d.ts +24 -0
  27. package/dist/core/dashboard-server.js +224 -0
  28. package/dist/core/debug-output.d.ts +3 -0
  29. package/dist/core/debug-output.js +69 -0
  30. package/dist/core/debug-verify.d.ts +25 -0
  31. package/dist/core/debug-verify.js +253 -0
  32. package/dist/core/executor.d.ts +11 -0
  33. package/dist/core/executor.js +152 -0
  34. package/dist/core/index.d.ts +5 -0
  35. package/dist/core/index.js +5 -0
  36. package/dist/core/replay-engine.d.ts +20 -0
  37. package/dist/core/replay-engine.js +293 -0
  38. package/dist/core/replay-engine.test.d.ts +1 -0
  39. package/dist/core/replay-engine.test.js +482 -0
  40. package/dist/core/runtime-paths.d.ts +2 -0
  41. package/dist/core/runtime-paths.js +65 -0
  42. package/dist/core/runtime-paths.test.d.ts +1 -0
  43. package/dist/core/runtime-paths.test.js +50 -0
  44. package/dist/core/signature.d.ts +25 -0
  45. package/dist/core/signature.js +224 -0
  46. package/dist/core/signature.test.d.ts +1 -0
  47. package/dist/core/signature.test.js +38 -0
  48. package/dist/core/template-manager.d.ts +33 -0
  49. package/dist/core/template-manager.js +313 -0
  50. package/dist/core/template-manager.test.d.ts +1 -0
  51. package/dist/core/template-manager.test.js +236 -0
  52. package/dist/index.cjs +135 -20
  53. package/dist/index.js +123 -8
  54. package/dist/types/index.d.ts +312 -0
  55. package/dist/types/index.js +87 -0
  56. package/package.json +1 -1
@@ -0,0 +1,236 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { existsSync, rmSync, mkdirSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { TemplateManager } from "./template-manager.js";
6
+ describe("TemplateManager", () => {
7
+ let tempDir;
8
+ let manager;
9
+ beforeEach(() => {
10
+ tempDir = join(tmpdir(), `better-webhook-test-${Date.now()}`);
11
+ mkdirSync(tempDir, { recursive: true });
12
+ manager = new TemplateManager(tempDir);
13
+ });
14
+ afterEach(() => {
15
+ if (existsSync(tempDir)) {
16
+ rmSync(tempDir, { recursive: true, force: true });
17
+ }
18
+ });
19
+ describe("templateExists", () => {
20
+ it("should return false when template does not exist", () => {
21
+ expect(manager.templateExists("non-existent")).toBe(false);
22
+ });
23
+ it("should return true when template exists", () => {
24
+ const template = {
25
+ url: "http://localhost:3000/webhooks",
26
+ method: "POST",
27
+ headers: [],
28
+ body: { test: true },
29
+ provider: "github",
30
+ event: "push",
31
+ };
32
+ manager.saveUserTemplate(template, { id: "test-template" });
33
+ expect(manager.templateExists("test-template")).toBe(true);
34
+ });
35
+ });
36
+ describe("saveUserTemplate", () => {
37
+ it("should save a template with explicit ID", () => {
38
+ const template = {
39
+ url: "http://localhost:3000/webhooks",
40
+ method: "POST",
41
+ headers: [{ key: "content-type", value: "application/json" }],
42
+ body: { action: "created" },
43
+ provider: "github",
44
+ event: "push",
45
+ };
46
+ const result = manager.saveUserTemplate(template, {
47
+ id: "my-template",
48
+ name: "My Template",
49
+ description: "A test template",
50
+ });
51
+ expect(result.id).toBe("my-template");
52
+ expect(result.filePath).toContain("my-template.json");
53
+ expect(existsSync(result.filePath)).toBe(true);
54
+ const saved = JSON.parse(readFileSync(result.filePath, "utf-8"));
55
+ expect(saved._metadata.id).toBe("my-template");
56
+ expect(saved._metadata.name).toBe("My Template");
57
+ expect(saved._metadata.description).toBe("A test template");
58
+ expect(saved._metadata.source).toBe("capture");
59
+ expect(saved._metadata.provider).toBe("github");
60
+ expect(saved._metadata.event).toBe("push");
61
+ });
62
+ it("should auto-generate ID from provider and event", () => {
63
+ const template = {
64
+ url: "http://localhost:3000/webhooks",
65
+ method: "POST",
66
+ headers: [],
67
+ body: {},
68
+ provider: "stripe",
69
+ event: "payment.succeeded",
70
+ };
71
+ const result = manager.saveUserTemplate(template);
72
+ expect(result.id).toBe("stripe-payment.succeeded");
73
+ });
74
+ it("should generate unique ID when base ID already exists", () => {
75
+ const template = {
76
+ url: "http://localhost:3000/webhooks",
77
+ method: "POST",
78
+ headers: [],
79
+ body: {},
80
+ provider: "github",
81
+ event: "push",
82
+ };
83
+ const result1 = manager.saveUserTemplate(template);
84
+ expect(result1.id).toBe("github-push");
85
+ const result2 = manager.saveUserTemplate(template);
86
+ expect(result2.id).toBe("github-push-1");
87
+ const result3 = manager.saveUserTemplate(template);
88
+ expect(result3.id).toBe("github-push-2");
89
+ });
90
+ it("should throw error when template exists and overwrite is false", () => {
91
+ const template = {
92
+ url: "http://localhost:3000/webhooks",
93
+ method: "POST",
94
+ headers: [],
95
+ body: {},
96
+ };
97
+ manager.saveUserTemplate(template, { id: "existing-template" });
98
+ expect(() => {
99
+ manager.saveUserTemplate(template, { id: "existing-template" });
100
+ }).toThrow('Template with ID "existing-template" already exists');
101
+ });
102
+ it("should overwrite when overwrite option is true", () => {
103
+ const template1 = {
104
+ url: "http://localhost:3000/webhooks",
105
+ method: "POST",
106
+ headers: [],
107
+ body: { version: 1 },
108
+ };
109
+ const template2 = {
110
+ url: "http://localhost:3000/webhooks",
111
+ method: "POST",
112
+ headers: [],
113
+ body: { version: 2 },
114
+ };
115
+ manager.saveUserTemplate(template1, { id: "my-template" });
116
+ const result = manager.saveUserTemplate(template2, {
117
+ id: "my-template",
118
+ overwrite: true,
119
+ });
120
+ const saved = JSON.parse(readFileSync(result.filePath, "utf-8"));
121
+ expect(saved.body.version).toBe(2);
122
+ });
123
+ it("should use custom provider as default", () => {
124
+ const template = {
125
+ url: "http://localhost:3000/webhooks",
126
+ method: "POST",
127
+ headers: [],
128
+ body: {},
129
+ };
130
+ const result = manager.saveUserTemplate(template);
131
+ expect(result.id).toBe("custom-webhook");
132
+ const saved = JSON.parse(readFileSync(result.filePath, "utf-8"));
133
+ expect(saved._metadata.provider).toBe("custom");
134
+ });
135
+ it("should use event option over template event", () => {
136
+ const template = {
137
+ url: "http://localhost:3000/webhooks",
138
+ method: "POST",
139
+ headers: [],
140
+ body: {},
141
+ provider: "github",
142
+ event: "push",
143
+ };
144
+ const result = manager.saveUserTemplate(template, {
145
+ event: "pull_request",
146
+ });
147
+ expect(result.id).toBe("github-pull_request");
148
+ const saved = JSON.parse(readFileSync(result.filePath, "utf-8"));
149
+ expect(saved._metadata.event).toBe("pull_request");
150
+ });
151
+ it("should create provider subdirectory", () => {
152
+ const template = {
153
+ url: "http://localhost:3000/webhooks",
154
+ method: "POST",
155
+ headers: [],
156
+ body: {},
157
+ provider: "slack",
158
+ event: "message",
159
+ };
160
+ const result = manager.saveUserTemplate(template);
161
+ expect(result.filePath).toContain("slack");
162
+ expect(existsSync(join(tempDir, "templates", "slack"))).toBe(true);
163
+ });
164
+ it("should include createdAt in metadata", () => {
165
+ const template = {
166
+ url: "http://localhost:3000/webhooks",
167
+ method: "POST",
168
+ headers: [],
169
+ body: {},
170
+ };
171
+ const beforeSave = new Date();
172
+ const result = manager.saveUserTemplate(template, { id: "test" });
173
+ const afterSave = new Date();
174
+ const saved = JSON.parse(readFileSync(result.filePath, "utf-8"));
175
+ const createdAt = new Date(saved._metadata.createdAt);
176
+ expect(createdAt.getTime()).toBeGreaterThanOrEqual(beforeSave.getTime());
177
+ expect(createdAt.getTime()).toBeLessThanOrEqual(afterSave.getTime());
178
+ });
179
+ });
180
+ describe("integration with listLocalTemplates", () => {
181
+ it("should list saved user templates", () => {
182
+ const template = {
183
+ url: "http://localhost:3000/webhooks",
184
+ method: "POST",
185
+ headers: [],
186
+ body: { test: true },
187
+ provider: "github",
188
+ event: "push",
189
+ };
190
+ manager.saveUserTemplate(template, {
191
+ id: "user-template",
192
+ name: "User Template",
193
+ });
194
+ const templates = manager.listLocalTemplates();
195
+ expect(templates).toHaveLength(1);
196
+ expect(templates[0].id).toBe("user-template");
197
+ expect(templates[0].metadata.name).toBe("User Template");
198
+ expect(templates[0].metadata.source).toBe("capture");
199
+ });
200
+ });
201
+ describe("integration with getLocalTemplate", () => {
202
+ it("should retrieve saved user template by ID", () => {
203
+ const template = {
204
+ url: "http://localhost:3000/webhooks",
205
+ method: "POST",
206
+ headers: [{ key: "x-custom", value: "test" }],
207
+ body: { data: "value" },
208
+ provider: "stripe",
209
+ event: "invoice.paid",
210
+ };
211
+ manager.saveUserTemplate(template, { id: "stripe-test" });
212
+ const retrieved = manager.getLocalTemplate("stripe-test");
213
+ expect(retrieved).not.toBeNull();
214
+ expect(retrieved.id).toBe("stripe-test");
215
+ expect(retrieved.template.body).toEqual({ data: "value" });
216
+ expect(retrieved.template.headers).toEqual([
217
+ { key: "x-custom", value: "test" },
218
+ ]);
219
+ });
220
+ });
221
+ describe("integration with deleteLocalTemplate", () => {
222
+ it("should delete saved user template", () => {
223
+ const template = {
224
+ url: "http://localhost:3000/webhooks",
225
+ method: "POST",
226
+ headers: [],
227
+ body: {},
228
+ };
229
+ manager.saveUserTemplate(template, { id: "to-delete" });
230
+ expect(manager.templateExists("to-delete")).toBe(true);
231
+ const deleted = manager.deleteLocalTemplate("to-delete");
232
+ expect(deleted).toBe(true);
233
+ expect(manager.templateExists("to-delete")).toBe(false);
234
+ });
235
+ });
236
+ });
package/dist/index.cjs CHANGED
@@ -1764,7 +1764,9 @@ function resolveRuntimePackageVersion(runtimeDir) {
1764
1764
  continue;
1765
1765
  }
1766
1766
  visitedRoots.add(cliPackageRoot);
1767
- const version = readPackageVersion(import_node_path2.default.join(cliPackageRoot, "package.json"));
1767
+ const version = readPackageVersion(
1768
+ import_node_path2.default.join(cliPackageRoot, "package.json")
1769
+ );
1768
1770
  if (version) {
1769
1771
  return version;
1770
1772
  }
@@ -3156,6 +3158,7 @@ var WebhookProviderSchema = import_zod.z.enum([
3156
3158
  "shopify",
3157
3159
  "twilio",
3158
3160
  "ragie",
3161
+ "recall",
3159
3162
  "sendgrid",
3160
3163
  "slack",
3161
3164
  "discord",
@@ -3849,25 +3852,26 @@ var import_prompts2 = __toESM(require("prompts"), 1);
3849
3852
  var import_undici2 = require("undici");
3850
3853
 
3851
3854
  // src/core/signature.ts
3852
- var import_crypto = require("crypto");
3855
+ var import_node_buffer = require("buffer");
3856
+ var import_node_crypto = require("crypto");
3853
3857
  function generateStripeSignature(payload, secret, timestamp) {
3854
3858
  const ts = timestamp || Math.floor(Date.now() / 1e3);
3855
3859
  const signedPayload = `${ts}.${payload}`;
3856
- const signature = (0, import_crypto.createHmac)("sha256", secret).update(signedPayload).digest("hex");
3860
+ const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(signedPayload).digest("hex");
3857
3861
  return {
3858
3862
  header: "Stripe-Signature",
3859
3863
  value: `t=${ts},v1=${signature}`
3860
3864
  };
3861
3865
  }
3862
3866
  function generateGitHubSignature(payload, secret) {
3863
- const signature = (0, import_crypto.createHmac)("sha256", secret).update(payload).digest("hex");
3867
+ const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(payload).digest("hex");
3864
3868
  return {
3865
3869
  header: "X-Hub-Signature-256",
3866
3870
  value: `sha256=${signature}`
3867
3871
  };
3868
3872
  }
3869
3873
  function generateShopifySignature(payload, secret) {
3870
- const signature = (0, import_crypto.createHmac)("sha256", secret).update(payload).digest("base64");
3874
+ const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(payload).digest("base64");
3871
3875
  return {
3872
3876
  header: "X-Shopify-Hmac-SHA256",
3873
3877
  value: signature
@@ -3875,7 +3879,7 @@ function generateShopifySignature(payload, secret) {
3875
3879
  }
3876
3880
  function generateTwilioSignature(payload, secret, url) {
3877
3881
  const signatureInput = url + payload;
3878
- const signature = (0, import_crypto.createHmac)("sha1", secret).update(signatureInput).digest("base64");
3882
+ const signature = (0, import_node_crypto.createHmac)("sha1", secret).update(signatureInput).digest("base64");
3879
3883
  return {
3880
3884
  header: "X-Twilio-Signature",
3881
3885
  value: signature
@@ -3884,14 +3888,14 @@ function generateTwilioSignature(payload, secret, url) {
3884
3888
  function generateSlackSignature(payload, secret, timestamp) {
3885
3889
  const ts = timestamp || Math.floor(Date.now() / 1e3);
3886
3890
  const signatureBaseString = `v0:${ts}:${payload}`;
3887
- const signature = (0, import_crypto.createHmac)("sha256", secret).update(signatureBaseString).digest("hex");
3891
+ const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(signatureBaseString).digest("hex");
3888
3892
  return {
3889
3893
  header: "X-Slack-Signature",
3890
3894
  value: `v0=${signature}`
3891
3895
  };
3892
3896
  }
3893
3897
  function generateLinearSignature(payload, secret) {
3894
- const signature = (0, import_crypto.createHmac)("sha256", secret).update(payload).digest("hex");
3898
+ const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(payload).digest("hex");
3895
3899
  return {
3896
3900
  header: "Linear-Signature",
3897
3901
  value: signature
@@ -3901,7 +3905,7 @@ function generateClerkSignature(payload, secret, timestamp, webhookId) {
3901
3905
  const ts = timestamp || Math.floor(Date.now() / 1e3);
3902
3906
  const msgId = webhookId || `msg_${Date.now()}`;
3903
3907
  const signedPayload = `${msgId}.${ts}.${payload}`;
3904
- const signature = (0, import_crypto.createHmac)("sha256", secret).update(signedPayload).digest("base64");
3908
+ const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(signedPayload).digest("base64");
3905
3909
  return {
3906
3910
  header: "Svix-Signature",
3907
3911
  value: `v1,${signature}`
@@ -3910,19 +3914,38 @@ function generateClerkSignature(payload, secret, timestamp, webhookId) {
3910
3914
  function generateSendGridSignature(payload, secret, timestamp) {
3911
3915
  const ts = timestamp || Math.floor(Date.now() / 1e3);
3912
3916
  const signedPayload = `${ts}${payload}`;
3913
- const signature = (0, import_crypto.createHmac)("sha256", secret).update(signedPayload).digest("base64");
3917
+ const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(signedPayload).digest("base64");
3914
3918
  return {
3915
3919
  header: "X-Twilio-Email-Event-Webhook-Signature",
3916
3920
  value: signature
3917
3921
  };
3918
3922
  }
3919
3923
  function generateRagieSignature(payload, secret) {
3920
- const signature = (0, import_crypto.createHmac)("sha256", secret).update(payload).digest("hex");
3924
+ const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(payload).digest("hex");
3921
3925
  return {
3922
3926
  header: "X-Signature",
3923
3927
  value: signature
3924
3928
  };
3925
3929
  }
3930
+ function generateRecallSignature(payload, secret, timestamp, webhookId) {
3931
+ if (!secret.startsWith("whsec_")) {
3932
+ throw new Error(
3933
+ "Recall signature generation requires a secret with the whsec_ prefix"
3934
+ );
3935
+ }
3936
+ const ts = timestamp ?? Math.floor(Date.now() / 1e3);
3937
+ const msgId = webhookId ?? `msg_${Date.now()}`;
3938
+ const key = import_node_buffer.Buffer.from(secret.slice("whsec_".length), "base64");
3939
+ if (key.length === 0) {
3940
+ throw new Error("Recall signing secret is invalid");
3941
+ }
3942
+ const signedPayload = `${msgId}.${ts}.${payload}`;
3943
+ const signature = (0, import_node_crypto.createHmac)("sha256", key).update(signedPayload).digest("base64");
3944
+ return {
3945
+ header: "Webhook-Signature",
3946
+ value: `v1,${signature}`
3947
+ };
3948
+ }
3926
3949
  function generateSignature(provider, payload, secret, options) {
3927
3950
  const timestamp = options?.timestamp;
3928
3951
  switch (provider) {
@@ -3952,6 +3975,13 @@ function generateSignature(provider, payload, secret, options) {
3952
3975
  return generateSendGridSignature(payload, secret, timestamp);
3953
3976
  case "ragie":
3954
3977
  return generateRagieSignature(payload, secret);
3978
+ case "recall":
3979
+ return generateRecallSignature(
3980
+ payload,
3981
+ secret,
3982
+ timestamp,
3983
+ options?.webhookId
3984
+ );
3955
3985
  case "discord":
3956
3986
  case "custom":
3957
3987
  default:
@@ -4040,6 +4070,13 @@ function getProviderHeaders(provider, options) {
4040
4070
  // Event type + nonce are included in the JSON body envelope.
4041
4071
  );
4042
4072
  break;
4073
+ case "recall":
4074
+ headers.push(
4075
+ { key: "Content-Type", value: "application/json" },
4076
+ { key: "Webhook-Id", value: options?.webhookId || `msg_${Date.now()}` },
4077
+ { key: "Webhook-Timestamp", value: String(timestamp) }
4078
+ );
4079
+ break;
4043
4080
  default:
4044
4081
  headers.push({ key: "Content-Type", value: "application/json" });
4045
4082
  }
@@ -4078,8 +4115,14 @@ async function executeWebhook(options) {
4078
4115
  }
4079
4116
  }
4080
4117
  if (options.secret && options.provider && bodyStr) {
4118
+ const timestampHeader = headers["Webhook-Timestamp"] || headers["webhook-timestamp"] || headers["Svix-Timestamp"] || headers["svix-timestamp"] || headers["X-Slack-Request-Timestamp"] || headers["x-slack-request-timestamp"] || headers["X-Twilio-Email-Event-Webhook-Timestamp"] || headers["x-twilio-email-event-webhook-timestamp"];
4119
+ const parsedTimestamp = timestampHeader ? Number.parseInt(timestampHeader, 10) : void 0;
4120
+ const timestamp = Number.isFinite(parsedTimestamp) ? parsedTimestamp : void 0;
4121
+ const webhookId = headers["Webhook-Id"] || headers["webhook-id"] || headers["Svix-Id"] || headers["svix-id"] || headers["X-GitHub-Delivery"] || headers["x-github-delivery"];
4081
4122
  const sig = generateSignature(options.provider, bodyStr, options.secret, {
4082
- url: options.url
4123
+ url: options.url,
4124
+ timestamp,
4125
+ webhookId
4083
4126
  });
4084
4127
  if (sig) {
4085
4128
  headers[sig.header] = sig.value;
@@ -4194,6 +4237,7 @@ function getSecretEnvVarName(provider) {
4194
4237
  shopify: "SHOPIFY_WEBHOOK_SECRET",
4195
4238
  twilio: "TWILIO_WEBHOOK_SECRET",
4196
4239
  ragie: "RAGIE_WEBHOOK_SECRET",
4240
+ recall: "RECALL_WEBHOOK_SECRET",
4197
4241
  slack: "SLACK_WEBHOOK_SECRET",
4198
4242
  linear: "LINEAR_WEBHOOK_SECRET",
4199
4243
  clerk: "CLERK_WEBHOOK_SECRET",
@@ -4379,7 +4423,7 @@ var import_http = require("http");
4379
4423
  var import_ws = require("ws");
4380
4424
  var import_fs2 = require("fs");
4381
4425
  var import_path2 = require("path");
4382
- var import_crypto2 = require("crypto");
4426
+ var import_crypto = require("crypto");
4383
4427
  var import_os2 = require("os");
4384
4428
  var CaptureServer = class {
4385
4429
  server = null;
@@ -4491,7 +4535,7 @@ var CaptureServer = class {
4491
4535
  return;
4492
4536
  }
4493
4537
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4494
- const id = (0, import_crypto2.randomUUID)();
4538
+ const id = (0, import_crypto.randomUUID)();
4495
4539
  const url = req.url || "/";
4496
4540
  const hostHeader = req.headers.host;
4497
4541
  const hostValue = typeof hostHeader === "string" ? hostHeader : "";
@@ -4631,6 +4675,12 @@ var CaptureServer = class {
4631
4675
  return "ragie";
4632
4676
  }
4633
4677
  }
4678
+ if (this.hasStandardWebhookHeaders(headers)) {
4679
+ const recallUserAgent = this.headerIncludes(headers["user-agent"], "recall");
4680
+ if (recallUserAgent || this.hasRecallStandardWebhookShape(body)) {
4681
+ return "recall";
4682
+ }
4683
+ }
4634
4684
  if (headers["x-shopify-hmac-sha256"] || headers["x-shopify-topic"]) {
4635
4685
  return "shopify";
4636
4686
  }
@@ -4650,6 +4700,9 @@ var CaptureServer = class {
4650
4700
  return "linear";
4651
4701
  }
4652
4702
  if (headers["svix-signature"]) {
4703
+ if (body && typeof body === "object" && "event" in body && typeof body.event === "string" && body.event.startsWith("bot.")) {
4704
+ return "recall";
4705
+ }
4653
4706
  return "clerk";
4654
4707
  }
4655
4708
  return void 0;
@@ -4665,6 +4718,57 @@ var CaptureServer = class {
4665
4718
  }
4666
4719
  }
4667
4720
  }
4721
+ hasStandardWebhookHeaders(headers) {
4722
+ return Boolean(
4723
+ headers["webhook-signature"] || headers["webhook-id"] && headers["webhook-timestamp"]
4724
+ );
4725
+ }
4726
+ hasRecallStandardWebhookShape(body) {
4727
+ if (!body || typeof body !== "object") {
4728
+ return false;
4729
+ }
4730
+ const payload = body;
4731
+ if (this.hasRecallEventPrefix(payload.event)) {
4732
+ return true;
4733
+ }
4734
+ if (this.hasRecallResourceCombination(payload)) {
4735
+ return true;
4736
+ }
4737
+ const nestedData = payload.data;
4738
+ if (nestedData && typeof nestedData === "object") {
4739
+ return this.hasRecallResourceCombination(
4740
+ nestedData
4741
+ );
4742
+ }
4743
+ return false;
4744
+ }
4745
+ hasRecallEventPrefix(event) {
4746
+ if (typeof event !== "string") {
4747
+ return false;
4748
+ }
4749
+ return ["bot.", "transcript.", "participant_events."].some(
4750
+ (prefix) => event.startsWith(prefix)
4751
+ );
4752
+ }
4753
+ hasRecallResourceCombination(payload) {
4754
+ const hasRealtimeEndpoint = "realtime_endpoint" in payload;
4755
+ const hasRecording = "recording" in payload;
4756
+ const hasParticipantEvents = "participant_events" in payload;
4757
+ const hasTranscript = "transcript" in payload;
4758
+ return hasRealtimeEndpoint && hasRecording && (hasParticipantEvents || hasTranscript);
4759
+ }
4760
+ headerIncludes(headerValue, searchText) {
4761
+ const normalizedSearchText = searchText.toLowerCase();
4762
+ if (typeof headerValue === "string") {
4763
+ return headerValue.toLowerCase().includes(normalizedSearchText);
4764
+ }
4765
+ if (Array.isArray(headerValue)) {
4766
+ return headerValue.some(
4767
+ (value) => value.toLowerCase().includes(normalizedSearchText)
4768
+ );
4769
+ }
4770
+ return false;
4771
+ }
4668
4772
  /**
4669
4773
  * Send message to a specific client
4670
4774
  */
@@ -4866,6 +4970,7 @@ var ReplayEngine = class {
4866
4970
  "x-twilio-signature",
4867
4971
  "x-slack-signature",
4868
4972
  "svix-signature",
4973
+ "webhook-signature",
4869
4974
  "linear-signature"
4870
4975
  ];
4871
4976
  const headers = [];
@@ -4937,10 +5042,19 @@ var ReplayEngine = class {
4937
5042
  }
4938
5043
  if (capture2.provider === "ragie" && capture2.body) {
4939
5044
  const body = capture2.body;
5045
+ if (typeof body.type === "string") {
5046
+ return body.type;
5047
+ }
4940
5048
  if (typeof body.event_type === "string") {
4941
5049
  return body.event_type;
4942
5050
  }
4943
5051
  }
5052
+ if (capture2.provider === "recall" && capture2.body) {
5053
+ const body = capture2.body;
5054
+ if (typeof body.event === "string") {
5055
+ return body.event;
5056
+ }
5057
+ }
4944
5058
  const shopifyTopic = headers["x-shopify-topic"];
4945
5059
  if (shopifyTopic) {
4946
5060
  return Array.isArray(shopifyTopic) ? shopifyTopic[0] : shopifyTopic;
@@ -5527,6 +5641,7 @@ function getSecretEnvVarName2(provider) {
5527
5641
  shopify: "SHOPIFY_WEBHOOK_SECRET",
5528
5642
  twilio: "TWILIO_WEBHOOK_SECRET",
5529
5643
  ragie: "RAGIE_WEBHOOK_SECRET",
5644
+ recall: "RECALL_WEBHOOK_SECRET",
5530
5645
  slack: "SLACK_WEBHOOK_SECRET",
5531
5646
  linear: "LINEAR_WEBHOOK_SECRET",
5532
5647
  clerk: "CLERK_WEBHOOK_SECRET",
@@ -6104,13 +6219,13 @@ var dashboard = new import_commander6.Command().name("dashboard").description("S
6104
6219
  process.exitCode = 1;
6105
6220
  return;
6106
6221
  }
6222
+ const capturePort = Number.parseInt(String(options.capturePort), 10);
6223
+ if (!Number.isFinite(capturePort) || capturePort < 0 || capturePort > 65535) {
6224
+ console.error(source_default.red("Invalid capture port number"));
6225
+ process.exitCode = 1;
6226
+ return;
6227
+ }
6107
6228
  try {
6108
- const capturePort = Number.parseInt(String(options.capturePort), 10);
6109
- if (!Number.isFinite(capturePort) || capturePort < 0 || capturePort > 65535) {
6110
- console.error(source_default.red("Invalid capture port number"));
6111
- process.exitCode = 1;
6112
- return;
6113
- }
6114
6229
  const verbose = Boolean(options.verbose || options.debug);
6115
6230
  const { url, server, capture: capture2 } = await startDashboardServer({
6116
6231
  host: options.host,