@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.
- package/dist/_binary_entry.js +29 -0
- package/dist/commands/capture.d.ts +2 -0
- package/dist/commands/capture.js +33 -0
- package/dist/commands/captures.d.ts +2 -0
- package/dist/commands/captures.js +316 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +70 -0
- package/dist/commands/index.d.ts +6 -0
- package/dist/commands/index.js +6 -0
- package/dist/commands/replay.d.ts +2 -0
- package/dist/commands/replay.js +140 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +182 -0
- package/dist/commands/templates.d.ts +2 -0
- package/dist/commands/templates.js +285 -0
- package/dist/core/capture-server.d.ts +37 -0
- package/dist/core/capture-server.js +400 -0
- package/dist/core/capture-server.test.d.ts +1 -0
- package/dist/core/capture-server.test.js +86 -0
- package/dist/core/cli-version.d.ts +1 -0
- package/dist/core/cli-version.js +30 -0
- package/dist/core/cli-version.test.d.ts +1 -0
- package/dist/core/cli-version.test.js +42 -0
- package/dist/core/dashboard-api.d.ts +8 -0
- package/dist/core/dashboard-api.js +333 -0
- package/dist/core/dashboard-server.d.ts +24 -0
- package/dist/core/dashboard-server.js +224 -0
- package/dist/core/debug-output.d.ts +3 -0
- package/dist/core/debug-output.js +69 -0
- package/dist/core/debug-verify.d.ts +25 -0
- package/dist/core/debug-verify.js +253 -0
- package/dist/core/executor.d.ts +11 -0
- package/dist/core/executor.js +152 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +5 -0
- package/dist/core/replay-engine.d.ts +20 -0
- package/dist/core/replay-engine.js +293 -0
- package/dist/core/replay-engine.test.d.ts +1 -0
- package/dist/core/replay-engine.test.js +482 -0
- package/dist/core/runtime-paths.d.ts +2 -0
- package/dist/core/runtime-paths.js +65 -0
- package/dist/core/runtime-paths.test.d.ts +1 -0
- package/dist/core/runtime-paths.test.js +50 -0
- package/dist/core/signature.d.ts +25 -0
- package/dist/core/signature.js +224 -0
- package/dist/core/signature.test.d.ts +1 -0
- package/dist/core/signature.test.js +38 -0
- package/dist/core/template-manager.d.ts +33 -0
- package/dist/core/template-manager.js +313 -0
- package/dist/core/template-manager.test.d.ts +1 -0
- package/dist/core/template-manager.test.js +236 -0
- package/dist/index.cjs +135 -20
- package/dist/index.js +123 -8
- package/dist/types/index.d.ts +312 -0
- package/dist/types/index.js +87 -0
- 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(
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|