@better-webhook/cli 3.4.3 → 3.5.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 (41) hide show
  1. package/README.md +35 -0
  2. package/dist/dashboard/assets/index-CxrRCNTh.css +1 -0
  3. package/dist/dashboard/assets/index-Dlqdzwyc.js +23 -0
  4. package/dist/dashboard/index.html +2 -2
  5. package/dist/index.cjs +341 -24
  6. package/dist/index.js +341 -24
  7. package/package.json +5 -4
  8. package/dist/commands/capture.d.ts +0 -2
  9. package/dist/commands/capture.js +0 -30
  10. package/dist/commands/captures.d.ts +0 -2
  11. package/dist/commands/captures.js +0 -217
  12. package/dist/commands/dashboard.d.ts +0 -2
  13. package/dist/commands/dashboard.js +0 -65
  14. package/dist/commands/index.d.ts +0 -6
  15. package/dist/commands/index.js +0 -6
  16. package/dist/commands/replay.d.ts +0 -2
  17. package/dist/commands/replay.js +0 -140
  18. package/dist/commands/run.d.ts +0 -2
  19. package/dist/commands/run.js +0 -181
  20. package/dist/commands/templates.d.ts +0 -2
  21. package/dist/commands/templates.js +0 -285
  22. package/dist/core/capture-server.d.ts +0 -31
  23. package/dist/core/capture-server.js +0 -298
  24. package/dist/core/dashboard-api.d.ts +0 -8
  25. package/dist/core/dashboard-api.js +0 -271
  26. package/dist/core/dashboard-server.d.ts +0 -20
  27. package/dist/core/dashboard-server.js +0 -124
  28. package/dist/core/executor.d.ts +0 -11
  29. package/dist/core/executor.js +0 -130
  30. package/dist/core/index.d.ts +0 -5
  31. package/dist/core/index.js +0 -5
  32. package/dist/core/replay-engine.d.ts +0 -18
  33. package/dist/core/replay-engine.js +0 -208
  34. package/dist/core/signature.d.ts +0 -24
  35. package/dist/core/signature.js +0 -199
  36. package/dist/core/template-manager.d.ts +0 -24
  37. package/dist/core/template-manager.js +0 -246
  38. package/dist/dashboard/assets/index-BSfTbn4Y.js +0 -23
  39. package/dist/dashboard/assets/index-zDTVdss_.css +0 -1
  40. package/dist/types/index.d.ts +0 -299
  41. package/dist/types/index.js +0 -86
@@ -1,271 +0,0 @@
1
- import express from "express";
2
- import { z } from "zod";
3
- import { TemplateManager } from "./template-manager.js";
4
- import { ReplayEngine } from "./replay-engine.js";
5
- import { executeTemplate } from "./executor.js";
6
- import { HeaderEntrySchema, HttpMethodSchema, } from "../types/index.js";
7
- function jsonError(res, status, message) {
8
- return res.status(status).json({ error: message });
9
- }
10
- function getSecretEnvVarName(provider) {
11
- const envVarMap = {
12
- github: "GITHUB_WEBHOOK_SECRET",
13
- stripe: "STRIPE_WEBHOOK_SECRET",
14
- shopify: "SHOPIFY_WEBHOOK_SECRET",
15
- twilio: "TWILIO_WEBHOOK_SECRET",
16
- ragie: "RAGIE_WEBHOOK_SECRET",
17
- slack: "SLACK_WEBHOOK_SECRET",
18
- linear: "LINEAR_WEBHOOK_SECRET",
19
- clerk: "CLERK_WEBHOOK_SECRET",
20
- sendgrid: "SENDGRID_WEBHOOK_SECRET",
21
- discord: "DISCORD_WEBHOOK_SECRET",
22
- custom: "WEBHOOK_SECRET",
23
- };
24
- return envVarMap[provider] || "WEBHOOK_SECRET";
25
- }
26
- const ReplayBodySchema = z.object({
27
- captureId: z.string().min(1),
28
- targetUrl: z.string().min(1),
29
- method: HttpMethodSchema.optional(),
30
- headers: z.array(HeaderEntrySchema).optional(),
31
- });
32
- const TemplateDownloadBodySchema = z.object({
33
- id: z.string().min(1),
34
- });
35
- const RunTemplateBodySchema = z.object({
36
- templateId: z.string().min(1),
37
- url: z.string().min(1),
38
- secret: z.string().optional(),
39
- headers: z.array(HeaderEntrySchema).optional(),
40
- });
41
- export function createDashboardApiRouter(options = {}) {
42
- const router = express.Router();
43
- const replayEngine = new ReplayEngine(options.capturesDir);
44
- const templateManager = new TemplateManager(options.templatesBaseDir);
45
- const broadcast = options.broadcast;
46
- const broadcastCaptures = () => {
47
- if (!broadcast)
48
- return;
49
- const captures = replayEngine.listCaptures(200);
50
- broadcast({
51
- type: "captures_updated",
52
- payload: { captures, count: captures.length },
53
- });
54
- };
55
- const broadcastTemplates = async () => {
56
- if (!broadcast)
57
- return;
58
- const local = templateManager.listLocalTemplates();
59
- let remote = [];
60
- try {
61
- const index = await templateManager.fetchRemoteIndex(false);
62
- const localIds = new Set(local.map((t) => t.id));
63
- remote = index.templates.map((metadata) => ({
64
- metadata,
65
- isDownloaded: localIds.has(metadata.id),
66
- }));
67
- }
68
- catch {
69
- remote = [];
70
- }
71
- broadcast({
72
- type: "templates_updated",
73
- payload: { local, remote },
74
- });
75
- };
76
- router.get("/captures", (req, res) => {
77
- const limitRaw = typeof req.query.limit === "string" ? req.query.limit : "";
78
- const providerRaw = typeof req.query.provider === "string" ? req.query.provider : "";
79
- const qRaw = typeof req.query.q === "string" ? req.query.q : "";
80
- const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 50;
81
- if (!Number.isFinite(limit) || limit <= 0 || limit > 5000) {
82
- return jsonError(res, 400, "Invalid limit");
83
- }
84
- const q = qRaw.trim();
85
- const provider = providerRaw.trim();
86
- let captures = q
87
- ? replayEngine.searchCaptures(q)
88
- : replayEngine.listCaptures(Math.max(limit, 1000));
89
- if (provider) {
90
- captures = captures.filter((c) => (c.capture.provider || "").toLowerCase() === provider.toLowerCase());
91
- }
92
- captures = captures.slice(0, limit);
93
- return res.json({ captures, count: captures.length });
94
- });
95
- router.get("/captures/:id", (req, res) => {
96
- const id = req.params.id;
97
- if (!id) {
98
- return jsonError(res, 400, "Missing capture id");
99
- }
100
- const captureFile = replayEngine.getCapture(id);
101
- if (!captureFile) {
102
- return jsonError(res, 404, "Capture not found");
103
- }
104
- return res.json(captureFile);
105
- });
106
- router.delete("/captures/:id", (req, res) => {
107
- const id = req.params.id;
108
- if (!id) {
109
- return jsonError(res, 400, "Missing capture id");
110
- }
111
- const deleted = replayEngine.deleteCapture(id);
112
- if (!deleted) {
113
- return jsonError(res, 404, "Capture not found");
114
- }
115
- broadcastCaptures();
116
- return res.json({ success: true });
117
- });
118
- router.delete("/captures", (_req, res) => {
119
- const deleted = replayEngine.deleteAllCaptures();
120
- broadcastCaptures();
121
- return res.json({ success: true, deleted });
122
- });
123
- router.post("/replay", express.json({ limit: "5mb" }), async (req, res) => {
124
- const parsed = ReplayBodySchema.safeParse(req.body);
125
- if (!parsed.success) {
126
- return jsonError(res, 400, parsed.error.issues[0]?.message || "Invalid body");
127
- }
128
- const { captureId, targetUrl, method, headers } = parsed.data;
129
- try {
130
- new URL(targetUrl);
131
- }
132
- catch {
133
- return jsonError(res, 400, "Invalid targetUrl");
134
- }
135
- try {
136
- const result = await replayEngine.replay(captureId, {
137
- targetUrl,
138
- method,
139
- headers,
140
- });
141
- broadcast?.({
142
- type: "replay_result",
143
- payload: { captureId, targetUrl, result },
144
- });
145
- return res.json(result);
146
- }
147
- catch (error) {
148
- return jsonError(res, 400, error?.message || "Replay failed");
149
- }
150
- });
151
- router.get("/templates/local", (_req, res) => {
152
- const local = templateManager.listLocalTemplates();
153
- return res.json({ templates: local, count: local.length });
154
- });
155
- router.get("/templates/remote", async (req, res) => {
156
- const refresh = typeof req.query.refresh === "string"
157
- ? req.query.refresh === "1" ||
158
- req.query.refresh.toLowerCase() === "true"
159
- : false;
160
- try {
161
- const index = await templateManager.fetchRemoteIndex(refresh);
162
- const localIds = new Set(templateManager.listLocalTemplates().map((t) => t.id));
163
- const remote = index.templates.map((metadata) => ({
164
- metadata,
165
- isDownloaded: localIds.has(metadata.id),
166
- }));
167
- return res.json({ templates: remote, count: remote.length });
168
- }
169
- catch (error) {
170
- return jsonError(res, 500, error?.message || "Failed to fetch remote templates");
171
- }
172
- });
173
- router.post("/templates/download", express.json({ limit: "2mb" }), async (req, res) => {
174
- const parsed = TemplateDownloadBodySchema.safeParse(req.body);
175
- if (!parsed.success) {
176
- return jsonError(res, 400, parsed.error.issues[0]?.message || "Invalid body");
177
- }
178
- try {
179
- const template = await templateManager.downloadTemplate(parsed.data.id);
180
- void broadcastTemplates();
181
- return res.json({ success: true, template });
182
- }
183
- catch (error) {
184
- return jsonError(res, 400, error?.message || "Download failed");
185
- }
186
- });
187
- router.post("/templates/download-all", express.json({ limit: "1mb" }), async (_req, res) => {
188
- try {
189
- const index = await templateManager.fetchRemoteIndex(true);
190
- const localIds = new Set(templateManager.listLocalTemplates().map((t) => t.id));
191
- const toDownload = index.templates.filter((t) => !localIds.has(t.id));
192
- const downloaded = [];
193
- const failed = [];
194
- for (const t of toDownload) {
195
- try {
196
- await templateManager.downloadTemplate(t.id);
197
- downloaded.push(t.id);
198
- }
199
- catch (e) {
200
- failed.push({ id: t.id, error: e?.message || "Failed" });
201
- }
202
- }
203
- return res.json({
204
- success: true,
205
- total: index.templates.length,
206
- downloaded,
207
- failed,
208
- });
209
- }
210
- catch (error) {
211
- return jsonError(res, 500, error?.message || "Download-all failed");
212
- }
213
- });
214
- router.post("/run", express.json({ limit: "10mb" }), async (req, res) => {
215
- const parsed = RunTemplateBodySchema.safeParse(req.body);
216
- if (!parsed.success) {
217
- return jsonError(res, 400, parsed.error.issues[0]?.message || "Invalid body");
218
- }
219
- let { templateId, url, secret, headers } = parsed.data;
220
- try {
221
- new URL(url);
222
- }
223
- catch {
224
- return jsonError(res, 400, "Invalid url");
225
- }
226
- if (templateId.startsWith("remote:")) {
227
- templateId = templateId.slice("remote:".length);
228
- try {
229
- await templateManager.downloadTemplate(templateId);
230
- }
231
- catch (error) {
232
- return jsonError(res, 400, error?.message || "Failed to download template");
233
- }
234
- }
235
- let localTemplate = templateManager.getLocalTemplate(templateId);
236
- if (!localTemplate) {
237
- try {
238
- await templateManager.downloadTemplate(templateId);
239
- localTemplate = templateManager.getLocalTemplate(templateId);
240
- }
241
- catch {
242
- }
243
- }
244
- if (!localTemplate) {
245
- return jsonError(res, 404, "Template not found");
246
- }
247
- if (!secret && localTemplate.metadata.provider) {
248
- const envVarName = getSecretEnvVarName(localTemplate.metadata.provider);
249
- secret = process.env[envVarName];
250
- }
251
- const safeHeaders = headers?.length
252
- ? headers
253
- : undefined;
254
- try {
255
- const result = await executeTemplate(localTemplate.template, {
256
- url,
257
- secret,
258
- headers: safeHeaders,
259
- });
260
- broadcast?.({
261
- type: "replay_result",
262
- payload: { templateId, url, result },
263
- });
264
- return res.json(result);
265
- }
266
- catch (error) {
267
- return jsonError(res, 400, error?.message || "Run failed");
268
- }
269
- });
270
- return router;
271
- }
@@ -1,20 +0,0 @@
1
- import express from "express";
2
- import { type Server } from "http";
3
- import { type DashboardApiOptions } from "./dashboard-api.js";
4
- import { CaptureServer } from "./capture-server.js";
5
- export interface DashboardServerOptions extends DashboardApiOptions {
6
- host?: string;
7
- port?: number;
8
- captureHost?: string;
9
- capturePort?: number;
10
- startCapture?: boolean;
11
- }
12
- export declare function startDashboardServer(options?: DashboardServerOptions): Promise<{
13
- app: express.Express;
14
- server: Server;
15
- url: string;
16
- capture?: {
17
- server: CaptureServer;
18
- url: string;
19
- };
20
- }>;
@@ -1,124 +0,0 @@
1
- import express from "express";
2
- import { createServer } from "http";
3
- import { WebSocketServer } from "ws";
4
- import path from "path";
5
- import { existsSync } from "fs";
6
- import { fileURLToPath } from "url";
7
- import { createDashboardApiRouter, } from "./dashboard-api.js";
8
- import { CaptureServer } from "./capture-server.js";
9
- import { ReplayEngine } from "./replay-engine.js";
10
- import { TemplateManager } from "./template-manager.js";
11
- function resolveDashboardDistDir(runtimeDir) {
12
- const candidates = [
13
- path.resolve(runtimeDir, "dashboard"),
14
- path.resolve(runtimeDir, "..", "dashboard"),
15
- path.resolve(runtimeDir, "..", "..", "dist", "dashboard"),
16
- path.resolve(runtimeDir, "..", "..", "..", "dashboard", "dist"),
17
- ];
18
- for (const distDir of candidates) {
19
- const indexHtml = path.join(distDir, "index.html");
20
- if (existsSync(indexHtml)) {
21
- return { distDir, indexHtml };
22
- }
23
- }
24
- const details = candidates.map((p) => `- ${p}`).join("\n");
25
- throw new Error(`Dashboard UI build output not found.\n` +
26
- `Looked in:\n${details}\n\n` +
27
- `Build it with:\n` +
28
- `- pnpm --filter @better-webhook/dashboard build\n` +
29
- `- pnpm --filter @better-webhook/cli build\n`);
30
- }
31
- export async function startDashboardServer(options = {}) {
32
- const app = express();
33
- app.get("/health", (_req, res) => {
34
- res.json({ ok: true });
35
- });
36
- const clients = new Set();
37
- const broadcast = (message) => {
38
- const data = JSON.stringify(message);
39
- for (const client of clients) {
40
- if (client.readyState === 1) {
41
- client.send(data);
42
- }
43
- }
44
- };
45
- app.use("/api", createDashboardApiRouter({
46
- capturesDir: options.capturesDir,
47
- templatesBaseDir: options.templatesBaseDir,
48
- broadcast,
49
- }));
50
- const host = options.host || "localhost";
51
- const port = options.port ?? 4000;
52
- const runtimeDir = typeof __dirname !== "undefined"
53
- ?
54
- __dirname
55
- : path.dirname(fileURLToPath(import.meta.url));
56
- const { distDir: dashboardDistDir, indexHtml: dashboardIndexHtml } = resolveDashboardDistDir(runtimeDir);
57
- app.use(express.static(dashboardDistDir));
58
- app.get("*", (req, res, next) => {
59
- if (req.path.startsWith("/api") || req.path === "/health")
60
- return next();
61
- res.sendFile(dashboardIndexHtml, (err) => {
62
- if (err)
63
- next();
64
- });
65
- });
66
- const server = createServer(app);
67
- const wss = new WebSocketServer({ server, path: "/ws" });
68
- wss.on("connection", async (ws) => {
69
- clients.add(ws);
70
- ws.on("close", () => clients.delete(ws));
71
- ws.on("error", () => clients.delete(ws));
72
- const replayEngine = new ReplayEngine(options.capturesDir);
73
- const templateManager = new TemplateManager(options.templatesBaseDir);
74
- const captures = replayEngine.listCaptures(200);
75
- ws.send(JSON.stringify({
76
- type: "captures_updated",
77
- payload: { captures, count: captures.length },
78
- }));
79
- const local = templateManager.listLocalTemplates();
80
- let remote = [];
81
- try {
82
- const index = await templateManager.fetchRemoteIndex(true);
83
- const localIds = new Set(local.map((t) => t.id));
84
- remote = index.templates.map((metadata) => ({
85
- metadata,
86
- isDownloaded: localIds.has(metadata.id),
87
- }));
88
- }
89
- catch {
90
- remote = [];
91
- }
92
- ws.send(JSON.stringify({
93
- type: "templates_updated",
94
- payload: { local, remote },
95
- }));
96
- });
97
- await new Promise((resolve, reject) => {
98
- server.listen(port, host, () => resolve());
99
- server.on("error", reject);
100
- });
101
- const url = `http://${host}:${port}`;
102
- let capture;
103
- const shouldStartCapture = options.startCapture !== false;
104
- if (shouldStartCapture) {
105
- const captureHost = options.captureHost || "0.0.0.0";
106
- const capturePort = options.capturePort ?? 3001;
107
- const captureServer = new CaptureServer({
108
- capturesDir: options.capturesDir,
109
- enableWebSocket: false,
110
- onCapture: ({ file, capture }) => {
111
- broadcast({
112
- type: "capture",
113
- payload: { file, capture },
114
- });
115
- },
116
- });
117
- const actualPort = await captureServer.start(capturePort, captureHost);
118
- capture = {
119
- server: captureServer,
120
- url: `http://${captureHost === "0.0.0.0" ? "localhost" : captureHost}:${actualPort}`,
121
- };
122
- }
123
- return { app, server, url, capture };
124
- }
@@ -1,11 +0,0 @@
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
- }
@@ -1,130 +0,0 @@
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 sig = generateSignature(options.provider, bodyStr, options.secret, {
26
- url: options.url,
27
- });
28
- if (sig) {
29
- headers[sig.header] = sig.value;
30
- }
31
- }
32
- if (!headers["Content-Type"] && !headers["content-type"]) {
33
- headers["Content-Type"] = "application/json";
34
- }
35
- try {
36
- const response = await request(options.url, {
37
- method: options.method || "POST",
38
- headers,
39
- body: bodyStr,
40
- headersTimeout: options.timeout || 30000,
41
- bodyTimeout: options.timeout || 30000,
42
- });
43
- const bodyText = await response.body.text();
44
- const duration = Date.now() - startTime;
45
- const responseHeaders = {};
46
- for (const [key, value] of Object.entries(response.headers)) {
47
- if (value !== undefined) {
48
- responseHeaders[key] = value;
49
- }
50
- }
51
- let json;
52
- try {
53
- json = JSON.parse(bodyText);
54
- }
55
- catch {
56
- }
57
- return {
58
- status: response.statusCode,
59
- statusText: getStatusText(response.statusCode),
60
- headers: responseHeaders,
61
- body: json ?? bodyText,
62
- bodyText,
63
- json,
64
- duration,
65
- };
66
- }
67
- catch (error) {
68
- const duration = Date.now() - startTime;
69
- throw new ExecutionError(error.message, duration);
70
- }
71
- }
72
- export async function executeTemplate(template, options = {}) {
73
- const targetUrl = options.url || template.url;
74
- if (!targetUrl) {
75
- throw new Error("No target URL specified. Use --url or set url in template.");
76
- }
77
- const mergedHeaders = [...(template.headers || [])];
78
- if (options.headers) {
79
- for (const h of options.headers) {
80
- const existingIdx = mergedHeaders.findIndex((mh) => mh.key.toLowerCase() === h.key.toLowerCase());
81
- if (existingIdx >= 0) {
82
- mergedHeaders[existingIdx] = h;
83
- }
84
- else {
85
- mergedHeaders.push(h);
86
- }
87
- }
88
- }
89
- return executeWebhook({
90
- url: targetUrl,
91
- method: template.method,
92
- headers: mergedHeaders,
93
- body: template.body,
94
- secret: options.secret,
95
- provider: template.provider,
96
- });
97
- }
98
- export class ExecutionError extends Error {
99
- duration;
100
- constructor(message, duration) {
101
- super(message);
102
- this.name = "ExecutionError";
103
- this.duration = duration;
104
- }
105
- }
106
- function getStatusText(code) {
107
- const statusTexts = {
108
- 200: "OK",
109
- 201: "Created",
110
- 202: "Accepted",
111
- 204: "No Content",
112
- 301: "Moved Permanently",
113
- 302: "Found",
114
- 304: "Not Modified",
115
- 400: "Bad Request",
116
- 401: "Unauthorized",
117
- 403: "Forbidden",
118
- 404: "Not Found",
119
- 405: "Method Not Allowed",
120
- 408: "Request Timeout",
121
- 409: "Conflict",
122
- 422: "Unprocessable Entity",
123
- 429: "Too Many Requests",
124
- 500: "Internal Server Error",
125
- 502: "Bad Gateway",
126
- 503: "Service Unavailable",
127
- 504: "Gateway Timeout",
128
- };
129
- return statusTexts[code] || "Unknown";
130
- }
@@ -1,5 +0,0 @@
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";
@@ -1,5 +0,0 @@
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";
@@ -1,18 +0,0 @@
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
- }): WebhookTemplate;
12
- getCaptureSummary(captureId: string): string;
13
- searchCaptures(query: string): CaptureFile[];
14
- getCapturesByProvider(provider: string): CaptureFile[];
15
- deleteCapture(captureId: string): boolean;
16
- deleteAllCaptures(): number;
17
- }
18
- export declare function getReplayEngine(capturesDir?: string): ReplayEngine;