@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,293 @@
1
+ import { existsSync, readFileSync, readdirSync, unlinkSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { executeWebhook } from "./executor.js";
5
+ export class ReplayEngine {
6
+ capturesDir;
7
+ constructor(capturesDir) {
8
+ this.capturesDir =
9
+ capturesDir || join(homedir(), ".better-webhook", "captures");
10
+ }
11
+ getCapturesDir() {
12
+ return this.capturesDir;
13
+ }
14
+ listCaptures(limit = 100) {
15
+ if (!existsSync(this.capturesDir)) {
16
+ return [];
17
+ }
18
+ const files = readdirSync(this.capturesDir)
19
+ .filter((f) => f.endsWith(".json"))
20
+ .sort()
21
+ .reverse()
22
+ .slice(0, limit);
23
+ const captures = [];
24
+ for (const file of files) {
25
+ try {
26
+ const content = readFileSync(join(this.capturesDir, file), "utf-8");
27
+ const capture = JSON.parse(content);
28
+ captures.push({ file, capture });
29
+ }
30
+ catch {
31
+ }
32
+ }
33
+ return captures;
34
+ }
35
+ getCapture(captureId) {
36
+ const captures = this.listCaptures(1000);
37
+ let found = captures.find((c) => c.capture.id === captureId);
38
+ if (found)
39
+ return found;
40
+ found = captures.find((c) => c.file.includes(captureId));
41
+ if (found)
42
+ return found;
43
+ found = captures.find((c) => c.capture.id.startsWith(captureId));
44
+ return found || null;
45
+ }
46
+ async replay(captureId, options) {
47
+ const captureFile = this.getCapture(captureId);
48
+ if (!captureFile) {
49
+ throw new Error(`Capture not found: ${captureId}`);
50
+ }
51
+ const { capture } = captureFile;
52
+ const headers = [];
53
+ const skipHeaders = [
54
+ "host",
55
+ "content-length",
56
+ "connection",
57
+ "accept-encoding",
58
+ ];
59
+ for (const [key, value] of Object.entries(capture.headers)) {
60
+ if (!skipHeaders.includes(key.toLowerCase())) {
61
+ const headerValue = Array.isArray(value) ? value.join(", ") : value;
62
+ if (headerValue) {
63
+ headers.push({ key, value: headerValue });
64
+ }
65
+ }
66
+ }
67
+ if (options.headers) {
68
+ for (const h of options.headers) {
69
+ const existingIdx = headers.findIndex((eh) => eh.key.toLowerCase() === h.key.toLowerCase());
70
+ if (existingIdx >= 0) {
71
+ headers[existingIdx] = h;
72
+ }
73
+ else {
74
+ headers.push(h);
75
+ }
76
+ }
77
+ }
78
+ const body = capture.rawBody || capture.body;
79
+ return executeWebhook({
80
+ url: options.targetUrl,
81
+ method: options.method || capture.method,
82
+ headers,
83
+ body,
84
+ });
85
+ }
86
+ captureToTemplate(captureId, options) {
87
+ const captureFile = this.getCapture(captureId);
88
+ if (!captureFile) {
89
+ throw new Error(`Capture not found: ${captureId}`);
90
+ }
91
+ const { capture } = captureFile;
92
+ const skipHeaders = [
93
+ "host",
94
+ "content-length",
95
+ "connection",
96
+ "accept-encoding",
97
+ "stripe-signature",
98
+ "x-hub-signature-256",
99
+ "x-hub-signature",
100
+ "x-shopify-hmac-sha256",
101
+ "x-twilio-signature",
102
+ "x-slack-signature",
103
+ "svix-signature",
104
+ "webhook-signature",
105
+ "linear-signature",
106
+ ];
107
+ const headers = [];
108
+ for (const [key, value] of Object.entries(capture.headers)) {
109
+ if (!skipHeaders.includes(key.toLowerCase())) {
110
+ const headerValue = Array.isArray(value) ? value.join(", ") : value;
111
+ if (headerValue) {
112
+ headers.push({ key, value: headerValue });
113
+ }
114
+ }
115
+ }
116
+ let body;
117
+ if (capture.body) {
118
+ body = capture.body;
119
+ }
120
+ else if (capture.rawBody) {
121
+ try {
122
+ body = JSON.parse(capture.rawBody);
123
+ }
124
+ catch {
125
+ body = capture.rawBody;
126
+ }
127
+ }
128
+ const event = options?.event || this.detectEvent(capture);
129
+ return {
130
+ url: options?.url || `http://localhost:3000${capture.path}`,
131
+ method: capture.method,
132
+ headers,
133
+ body,
134
+ provider: capture.provider,
135
+ event,
136
+ description: `Captured ${capture.provider || "webhook"} at ${capture.timestamp}`,
137
+ };
138
+ }
139
+ detectEvent(capture) {
140
+ const headers = capture.headers;
141
+ const githubEvent = headers["x-github-event"];
142
+ if (githubEvent) {
143
+ return Array.isArray(githubEvent) ? githubEvent[0] : githubEvent;
144
+ }
145
+ if (capture.provider === "stripe" && capture.body) {
146
+ const body = capture.body;
147
+ if (typeof body.type === "string") {
148
+ return body.type;
149
+ }
150
+ }
151
+ if (capture.provider === "slack" && capture.body) {
152
+ const body = capture.body;
153
+ if (typeof body.type === "string") {
154
+ return body.type;
155
+ }
156
+ const event = body.event;
157
+ if (event && typeof event.type === "string") {
158
+ return event.type;
159
+ }
160
+ }
161
+ if (capture.provider === "linear" && capture.body) {
162
+ const body = capture.body;
163
+ if (typeof body.type === "string") {
164
+ return body.type;
165
+ }
166
+ }
167
+ if (capture.provider === "clerk" && capture.body) {
168
+ const body = capture.body;
169
+ if (typeof body.type === "string") {
170
+ return body.type;
171
+ }
172
+ }
173
+ if (capture.provider === "ragie" && capture.body) {
174
+ const body = capture.body;
175
+ if (typeof body.type === "string") {
176
+ return body.type;
177
+ }
178
+ if (typeof body.event_type === "string") {
179
+ return body.event_type;
180
+ }
181
+ }
182
+ if (capture.provider === "recall" && capture.body) {
183
+ const body = capture.body;
184
+ if (typeof body.event === "string") {
185
+ return body.event;
186
+ }
187
+ }
188
+ const shopifyTopic = headers["x-shopify-topic"];
189
+ if (shopifyTopic) {
190
+ return Array.isArray(shopifyTopic) ? shopifyTopic[0] : shopifyTopic;
191
+ }
192
+ if (capture.provider === "sendgrid" && Array.isArray(capture.body)) {
193
+ const firstEvent = capture.body[0];
194
+ if (firstEvent && typeof firstEvent.event === "string") {
195
+ return firstEvent.event;
196
+ }
197
+ }
198
+ if (capture.provider === "discord" && capture.body) {
199
+ const body = capture.body;
200
+ if (typeof body.type === "number") {
201
+ return `type_${body.type}`;
202
+ }
203
+ }
204
+ if (capture.body && typeof capture.body === "object") {
205
+ const body = capture.body;
206
+ if (typeof body.type === "string") {
207
+ return body.type;
208
+ }
209
+ if (typeof body.event_type === "string") {
210
+ return body.event_type;
211
+ }
212
+ if (typeof body.event === "string") {
213
+ return body.event;
214
+ }
215
+ if (typeof body.action === "string") {
216
+ return body.action;
217
+ }
218
+ }
219
+ return undefined;
220
+ }
221
+ getCaptureSummary(captureId) {
222
+ const captureFile = this.getCapture(captureId);
223
+ if (!captureFile) {
224
+ return "Capture not found";
225
+ }
226
+ const { capture } = captureFile;
227
+ const lines = [];
228
+ lines.push(`ID: ${capture.id}`);
229
+ lines.push(`Timestamp: ${new Date(capture.timestamp).toLocaleString()}`);
230
+ lines.push(`Method: ${capture.method}`);
231
+ lines.push(`Path: ${capture.path}`);
232
+ if (capture.provider) {
233
+ lines.push(`Provider: ${capture.provider}`);
234
+ }
235
+ lines.push(`Content-Type: ${capture.contentType || "unknown"}`);
236
+ lines.push(`Body Size: ${capture.contentLength || 0} bytes`);
237
+ const headerCount = Object.keys(capture.headers).length;
238
+ lines.push(`Headers: ${headerCount}`);
239
+ return lines.join("\n");
240
+ }
241
+ searchCaptures(query) {
242
+ const queryLower = query.toLowerCase();
243
+ const captures = this.listCaptures(1000);
244
+ return captures.filter((c) => {
245
+ const { capture } = c;
246
+ return (capture.id.toLowerCase().includes(queryLower) ||
247
+ capture.path.toLowerCase().includes(queryLower) ||
248
+ capture.method.toLowerCase().includes(queryLower) ||
249
+ capture.provider?.toLowerCase().includes(queryLower) ||
250
+ c.file.toLowerCase().includes(queryLower));
251
+ });
252
+ }
253
+ getCapturesByProvider(provider) {
254
+ const captures = this.listCaptures(1000);
255
+ return captures.filter((c) => c.capture.provider === provider);
256
+ }
257
+ deleteCapture(captureId) {
258
+ const captureFile = this.getCapture(captureId);
259
+ if (!captureFile) {
260
+ return false;
261
+ }
262
+ try {
263
+ unlinkSync(join(this.capturesDir, captureFile.file));
264
+ return true;
265
+ }
266
+ catch {
267
+ return false;
268
+ }
269
+ }
270
+ deleteAllCaptures() {
271
+ if (!existsSync(this.capturesDir)) {
272
+ return 0;
273
+ }
274
+ const files = readdirSync(this.capturesDir).filter((f) => f.endsWith(".json"));
275
+ let deleted = 0;
276
+ for (const file of files) {
277
+ try {
278
+ unlinkSync(join(this.capturesDir, file));
279
+ deleted++;
280
+ }
281
+ catch {
282
+ }
283
+ }
284
+ return deleted;
285
+ }
286
+ }
287
+ let instance = null;
288
+ export function getReplayEngine(capturesDir) {
289
+ if (!instance) {
290
+ instance = new ReplayEngine(capturesDir);
291
+ }
292
+ return instance;
293
+ }
@@ -0,0 +1 @@
1
+ export {};