@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,253 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ function getHeader(headers, name) {
3
+ const value = headers[name.toLowerCase()];
4
+ return Array.isArray(value) ? value[0] : value;
5
+ }
6
+ function safeCompare(a, b) {
7
+ try {
8
+ const bufA = Buffer.from(a);
9
+ const bufB = Buffer.from(b);
10
+ if (bufA.length !== bufB.length) {
11
+ return false;
12
+ }
13
+ return timingSafeEqual(bufA, bufB);
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ export function debugGitHubVerify(rawBody, headers, secret) {
20
+ const headerName = "x-hub-signature-256";
21
+ const expectedSignature = getHeader(headers, headerName);
22
+ const signedPayload = rawBody;
23
+ const computed = createHmac("sha256", secret)
24
+ .update(signedPayload, "utf-8")
25
+ .digest("hex");
26
+ const computedSignature = `sha256=${computed}`;
27
+ const isValid = expectedSignature
28
+ ? safeCompare(expectedSignature, computedSignature)
29
+ : false;
30
+ return {
31
+ provider: "github",
32
+ rawBody,
33
+ signedPayload,
34
+ expectedSignature,
35
+ computedSignature,
36
+ isValid,
37
+ algorithm: "HMAC-SHA256",
38
+ headerName,
39
+ };
40
+ }
41
+ export function debugRagieVerify(rawBody, headers, secret) {
42
+ const headerName = "x-signature";
43
+ const expectedSignature = getHeader(headers, headerName);
44
+ const signedPayload = rawBody;
45
+ const computedSignature = createHmac("sha256", secret)
46
+ .update(signedPayload, "utf-8")
47
+ .digest("hex");
48
+ const isValid = expectedSignature
49
+ ? safeCompare(expectedSignature, computedSignature)
50
+ : false;
51
+ return {
52
+ provider: "ragie",
53
+ rawBody,
54
+ signedPayload,
55
+ expectedSignature,
56
+ computedSignature,
57
+ isValid,
58
+ algorithm: "HMAC-SHA256",
59
+ headerName,
60
+ };
61
+ }
62
+ export function debugStripeVerify(rawBody, headers, secret) {
63
+ const headerName = "stripe-signature";
64
+ const signatureHeader = getHeader(headers, headerName);
65
+ let timestamp;
66
+ let expectedSignature;
67
+ if (signatureHeader) {
68
+ const parts = signatureHeader.split(",");
69
+ for (const part of parts) {
70
+ const [key, value] = part.split("=");
71
+ if (key === "t")
72
+ timestamp = value;
73
+ if (key === "v1")
74
+ expectedSignature = value;
75
+ }
76
+ }
77
+ const signedPayload = timestamp ? `${timestamp}.${rawBody}` : rawBody;
78
+ const computedSignature = createHmac("sha256", secret)
79
+ .update(signedPayload, "utf-8")
80
+ .digest("hex");
81
+ const isValid = expectedSignature
82
+ ? safeCompare(expectedSignature, computedSignature)
83
+ : false;
84
+ return {
85
+ provider: "stripe",
86
+ rawBody,
87
+ signedPayload,
88
+ expectedSignature,
89
+ computedSignature,
90
+ isValid,
91
+ algorithm: "HMAC-SHA256",
92
+ headerName,
93
+ timestamp,
94
+ };
95
+ }
96
+ export function debugSlackVerify(rawBody, headers, secret) {
97
+ const headerName = "x-slack-signature";
98
+ const signatureHeader = getHeader(headers, headerName);
99
+ const timestamp = getHeader(headers, "x-slack-request-timestamp");
100
+ let expectedSignature;
101
+ if (signatureHeader?.startsWith("v0=")) {
102
+ expectedSignature = signatureHeader.slice(3);
103
+ }
104
+ const signedPayload = `v0:${timestamp || ""}:${rawBody}`;
105
+ const computedSignature = createHmac("sha256", secret)
106
+ .update(signedPayload, "utf-8")
107
+ .digest("hex");
108
+ const isValid = expectedSignature
109
+ ? safeCompare(expectedSignature, computedSignature)
110
+ : false;
111
+ return {
112
+ provider: "slack",
113
+ rawBody,
114
+ signedPayload,
115
+ expectedSignature: signatureHeader,
116
+ computedSignature: `v0=${computedSignature}`,
117
+ isValid,
118
+ algorithm: "HMAC-SHA256",
119
+ headerName,
120
+ timestamp,
121
+ };
122
+ }
123
+ export function debugLinearVerify(rawBody, headers, secret) {
124
+ const headerName = "linear-signature";
125
+ const expectedSignature = getHeader(headers, headerName);
126
+ const signedPayload = rawBody;
127
+ const computedSignature = createHmac("sha256", secret)
128
+ .update(signedPayload, "utf-8")
129
+ .digest("hex");
130
+ const isValid = expectedSignature
131
+ ? safeCompare(expectedSignature, computedSignature)
132
+ : false;
133
+ return {
134
+ provider: "linear",
135
+ rawBody,
136
+ signedPayload,
137
+ expectedSignature,
138
+ computedSignature,
139
+ isValid,
140
+ algorithm: "HMAC-SHA256",
141
+ headerName,
142
+ };
143
+ }
144
+ export function debugShopifyVerify(rawBody, headers, secret) {
145
+ const headerName = "x-shopify-hmac-sha256";
146
+ const expectedSignature = getHeader(headers, headerName);
147
+ const signedPayload = rawBody;
148
+ const computedSignature = createHmac("sha256", secret)
149
+ .update(signedPayload, "utf-8")
150
+ .digest("base64");
151
+ const isValid = expectedSignature
152
+ ? safeCompare(expectedSignature, computedSignature)
153
+ : false;
154
+ return {
155
+ provider: "shopify",
156
+ rawBody,
157
+ signedPayload,
158
+ expectedSignature,
159
+ computedSignature,
160
+ isValid,
161
+ algorithm: "HMAC-SHA256 (base64)",
162
+ headerName,
163
+ };
164
+ }
165
+ export function detectProviderFromHeaders(headers) {
166
+ if (getHeader(headers, "stripe-signature"))
167
+ return "stripe";
168
+ if (getHeader(headers, "x-hub-signature-256") ||
169
+ getHeader(headers, "x-github-event"))
170
+ return "github";
171
+ if (getHeader(headers, "x-signature"))
172
+ return "ragie";
173
+ if (getHeader(headers, "x-shopify-hmac-sha256"))
174
+ return "shopify";
175
+ if (getHeader(headers, "x-slack-signature"))
176
+ return "slack";
177
+ if (getHeader(headers, "linear-signature"))
178
+ return "linear";
179
+ if (getHeader(headers, "svix-signature"))
180
+ return "clerk";
181
+ if (getHeader(headers, "x-twilio-signature"))
182
+ return "twilio";
183
+ return undefined;
184
+ }
185
+ export function getSecretEnvVar(provider) {
186
+ const envVars = {
187
+ github: "GITHUB_WEBHOOK_SECRET",
188
+ stripe: "STRIPE_WEBHOOK_SECRET",
189
+ ragie: "RAGIE_WEBHOOK_SECRET",
190
+ slack: "SLACK_SIGNING_SECRET",
191
+ linear: "LINEAR_WEBHOOK_SECRET",
192
+ shopify: "SHOPIFY_WEBHOOK_SECRET",
193
+ twilio: "TWILIO_AUTH_TOKEN",
194
+ sendgrid: "SENDGRID_WEBHOOK_SECRET",
195
+ discord: "DISCORD_PUBLIC_KEY",
196
+ clerk: "CLERK_WEBHOOK_SECRET",
197
+ custom: "WEBHOOK_SECRET",
198
+ };
199
+ return envVars[provider] || "WEBHOOK_SECRET";
200
+ }
201
+ export function resolveSecret(cliSecret, provider) {
202
+ if (cliSecret)
203
+ return cliSecret;
204
+ if (provider) {
205
+ const envVar = getSecretEnvVar(provider);
206
+ const value = process.env[envVar];
207
+ if (value)
208
+ return value;
209
+ }
210
+ return process.env.WEBHOOK_SECRET;
211
+ }
212
+ export function debugVerify(provider, rawBody, headers, secret) {
213
+ const detectedProvider = provider || detectProviderFromHeaders(headers);
214
+ if (!detectedProvider) {
215
+ return {
216
+ provider: "unknown",
217
+ rawBody,
218
+ signedPayload: rawBody,
219
+ expectedSignature: undefined,
220
+ computedSignature: "",
221
+ isValid: false,
222
+ algorithm: "unknown",
223
+ headerName: "unknown",
224
+ error: "Could not detect provider from headers",
225
+ };
226
+ }
227
+ switch (detectedProvider) {
228
+ case "github":
229
+ return debugGitHubVerify(rawBody, headers, secret);
230
+ case "ragie":
231
+ return debugRagieVerify(rawBody, headers, secret);
232
+ case "stripe":
233
+ return debugStripeVerify(rawBody, headers, secret);
234
+ case "slack":
235
+ return debugSlackVerify(rawBody, headers, secret);
236
+ case "linear":
237
+ return debugLinearVerify(rawBody, headers, secret);
238
+ case "shopify":
239
+ return debugShopifyVerify(rawBody, headers, secret);
240
+ default:
241
+ return {
242
+ provider: detectedProvider,
243
+ rawBody,
244
+ signedPayload: rawBody,
245
+ expectedSignature: undefined,
246
+ computedSignature: "",
247
+ isValid: false,
248
+ algorithm: "unknown",
249
+ headerName: "unknown",
250
+ error: `Debug verification not implemented for provider: ${detectedProvider}`,
251
+ };
252
+ }
253
+ }
@@ -0,0 +1,11 @@
1
+ import type { WebhookExecutionOptions, WebhookExecutionResult, HeaderEntry, WebhookTemplate } from "../types/index.js";
2
+ export declare function executeWebhook(options: WebhookExecutionOptions): Promise<WebhookExecutionResult>;
3
+ export declare function executeTemplate(template: WebhookTemplate, options?: {
4
+ url?: string;
5
+ secret?: string;
6
+ headers?: HeaderEntry[];
7
+ }): Promise<WebhookExecutionResult>;
8
+ export declare class ExecutionError extends Error {
9
+ duration: number;
10
+ constructor(message: string, duration: number);
11
+ }
@@ -0,0 +1,152 @@
1
+ import { request } from "undici";
2
+ import { generateSignature, getProviderHeaders } from "./signature.js";
3
+ export async function executeWebhook(options) {
4
+ const startTime = Date.now();
5
+ let bodyStr;
6
+ if (options.body !== undefined) {
7
+ bodyStr =
8
+ typeof options.body === "string"
9
+ ? options.body
10
+ : JSON.stringify(options.body);
11
+ }
12
+ const headers = {};
13
+ if (options.provider) {
14
+ const providerHeaders = getProviderHeaders(options.provider);
15
+ for (const h of providerHeaders) {
16
+ headers[h.key] = h.value;
17
+ }
18
+ }
19
+ if (options.headers) {
20
+ for (const h of options.headers) {
21
+ headers[h.key] = h.value;
22
+ }
23
+ }
24
+ if (options.secret && options.provider && bodyStr) {
25
+ const timestampHeader = headers["Webhook-Timestamp"] ||
26
+ headers["webhook-timestamp"] ||
27
+ headers["Svix-Timestamp"] ||
28
+ headers["svix-timestamp"] ||
29
+ headers["X-Slack-Request-Timestamp"] ||
30
+ headers["x-slack-request-timestamp"] ||
31
+ headers["X-Twilio-Email-Event-Webhook-Timestamp"] ||
32
+ headers["x-twilio-email-event-webhook-timestamp"];
33
+ const parsedTimestamp = timestampHeader
34
+ ? Number.parseInt(timestampHeader, 10)
35
+ : undefined;
36
+ const timestamp = Number.isFinite(parsedTimestamp)
37
+ ? parsedTimestamp
38
+ : undefined;
39
+ const webhookId = headers["Webhook-Id"] ||
40
+ headers["webhook-id"] ||
41
+ headers["Svix-Id"] ||
42
+ headers["svix-id"] ||
43
+ headers["X-GitHub-Delivery"] ||
44
+ headers["x-github-delivery"];
45
+ const sig = generateSignature(options.provider, bodyStr, options.secret, {
46
+ url: options.url,
47
+ timestamp,
48
+ webhookId,
49
+ });
50
+ if (sig) {
51
+ headers[sig.header] = sig.value;
52
+ }
53
+ }
54
+ if (!headers["Content-Type"] && !headers["content-type"]) {
55
+ headers["Content-Type"] = "application/json";
56
+ }
57
+ try {
58
+ const response = await request(options.url, {
59
+ method: options.method || "POST",
60
+ headers,
61
+ body: bodyStr,
62
+ headersTimeout: options.timeout || 30000,
63
+ bodyTimeout: options.timeout || 30000,
64
+ });
65
+ const bodyText = await response.body.text();
66
+ const duration = Date.now() - startTime;
67
+ const responseHeaders = {};
68
+ for (const [key, value] of Object.entries(response.headers)) {
69
+ if (value !== undefined) {
70
+ responseHeaders[key] = value;
71
+ }
72
+ }
73
+ let json;
74
+ try {
75
+ json = JSON.parse(bodyText);
76
+ }
77
+ catch {
78
+ }
79
+ return {
80
+ status: response.statusCode,
81
+ statusText: getStatusText(response.statusCode),
82
+ headers: responseHeaders,
83
+ body: json ?? bodyText,
84
+ bodyText,
85
+ json,
86
+ duration,
87
+ };
88
+ }
89
+ catch (error) {
90
+ const duration = Date.now() - startTime;
91
+ throw new ExecutionError(error.message, duration);
92
+ }
93
+ }
94
+ export async function executeTemplate(template, options = {}) {
95
+ const targetUrl = options.url || template.url;
96
+ if (!targetUrl) {
97
+ throw new Error("No target URL specified. Use --url or set url in template.");
98
+ }
99
+ const mergedHeaders = [...(template.headers || [])];
100
+ if (options.headers) {
101
+ for (const h of options.headers) {
102
+ const existingIdx = mergedHeaders.findIndex((mh) => mh.key.toLowerCase() === h.key.toLowerCase());
103
+ if (existingIdx >= 0) {
104
+ mergedHeaders[existingIdx] = h;
105
+ }
106
+ else {
107
+ mergedHeaders.push(h);
108
+ }
109
+ }
110
+ }
111
+ return executeWebhook({
112
+ url: targetUrl,
113
+ method: template.method,
114
+ headers: mergedHeaders,
115
+ body: template.body,
116
+ secret: options.secret,
117
+ provider: template.provider,
118
+ });
119
+ }
120
+ export class ExecutionError extends Error {
121
+ duration;
122
+ constructor(message, duration) {
123
+ super(message);
124
+ this.name = "ExecutionError";
125
+ this.duration = duration;
126
+ }
127
+ }
128
+ function getStatusText(code) {
129
+ const statusTexts = {
130
+ 200: "OK",
131
+ 201: "Created",
132
+ 202: "Accepted",
133
+ 204: "No Content",
134
+ 301: "Moved Permanently",
135
+ 302: "Found",
136
+ 304: "Not Modified",
137
+ 400: "Bad Request",
138
+ 401: "Unauthorized",
139
+ 403: "Forbidden",
140
+ 404: "Not Found",
141
+ 405: "Method Not Allowed",
142
+ 408: "Request Timeout",
143
+ 409: "Conflict",
144
+ 422: "Unprocessable Entity",
145
+ 429: "Too Many Requests",
146
+ 500: "Internal Server Error",
147
+ 502: "Bad Gateway",
148
+ 503: "Service Unavailable",
149
+ 504: "Gateway Timeout",
150
+ };
151
+ return statusTexts[code] || "Unknown";
152
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./template-manager.js";
2
+ export * from "./signature.js";
3
+ export * from "./capture-server.js";
4
+ export * from "./replay-engine.js";
5
+ export * from "./executor.js";
@@ -0,0 +1,5 @@
1
+ export * from "./template-manager.js";
2
+ export * from "./signature.js";
3
+ export * from "./capture-server.js";
4
+ export * from "./replay-engine.js";
5
+ export * from "./executor.js";
@@ -0,0 +1,20 @@
1
+ import type { CaptureFile, ReplayOptions, WebhookExecutionResult, WebhookTemplate } from "../types/index.js";
2
+ export declare class ReplayEngine {
3
+ private capturesDir;
4
+ constructor(capturesDir?: string);
5
+ getCapturesDir(): string;
6
+ listCaptures(limit?: number): CaptureFile[];
7
+ getCapture(captureId: string): CaptureFile | null;
8
+ replay(captureId: string, options: ReplayOptions): Promise<WebhookExecutionResult>;
9
+ captureToTemplate(captureId: string, options?: {
10
+ url?: string;
11
+ event?: string;
12
+ }): WebhookTemplate;
13
+ private detectEvent;
14
+ getCaptureSummary(captureId: string): string;
15
+ searchCaptures(query: string): CaptureFile[];
16
+ getCapturesByProvider(provider: string): CaptureFile[];
17
+ deleteCapture(captureId: string): boolean;
18
+ deleteAllCaptures(): number;
19
+ }
20
+ export declare function getReplayEngine(capturesDir?: string): ReplayEngine;