@better-webhook/cli 3.0.0 → 3.2.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/README.md +30 -0
- package/dist/dashboard/assets/index--Ns7zZwD.css +1 -0
- package/dist/dashboard/assets/index-JTnjYuBA.js +23 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/dashboard/index.html +14 -0
- package/dist/dashboard/vite.svg +1 -0
- package/dist/index.cjs +608 -106
- package/dist/index.js +612 -116
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/index.ts
|
|
27
|
-
var
|
|
27
|
+
var import_commander7 = require("commander");
|
|
28
28
|
|
|
29
29
|
// src/commands/templates.ts
|
|
30
30
|
var import_commander = require("commander");
|
|
@@ -58,6 +58,7 @@ var WebhookProviderSchema = import_zod.z.enum([
|
|
|
58
58
|
"github",
|
|
59
59
|
"shopify",
|
|
60
60
|
"twilio",
|
|
61
|
+
"ragie",
|
|
61
62
|
"sendgrid",
|
|
62
63
|
"slack",
|
|
63
64
|
"discord",
|
|
@@ -192,8 +193,8 @@ var TemplateManager = class {
|
|
|
192
193
|
/**
|
|
193
194
|
* List all remote templates
|
|
194
195
|
*/
|
|
195
|
-
async listRemoteTemplates() {
|
|
196
|
-
const index = await this.fetchRemoteIndex();
|
|
196
|
+
async listRemoteTemplates(options) {
|
|
197
|
+
const index = await this.fetchRemoteIndex(!!options?.forceRefresh);
|
|
197
198
|
const localIds = new Set(this.listLocalTemplates().map((t) => t.id));
|
|
198
199
|
return index.templates.map((metadata) => ({
|
|
199
200
|
metadata,
|
|
@@ -391,7 +392,9 @@ var listCommand = new import_commander.Command().name("list").alias("ls").descri
|
|
|
391
392
|
const spinner = (0, import_ora.default)("Fetching remote templates...").start();
|
|
392
393
|
try {
|
|
393
394
|
const manager = getTemplateManager();
|
|
394
|
-
const templates2 = await manager.listRemoteTemplates(
|
|
395
|
+
const templates2 = await manager.listRemoteTemplates({
|
|
396
|
+
forceRefresh: !!options.refresh
|
|
397
|
+
});
|
|
395
398
|
spinner.stop();
|
|
396
399
|
if (templates2.length === 0) {
|
|
397
400
|
console.log(import_chalk.default.yellow("\u{1F4ED} No remote templates found."));
|
|
@@ -442,88 +445,98 @@ var listCommand = new import_commander.Command().name("list").alias("ls").descri
|
|
|
442
445
|
process.exitCode = 1;
|
|
443
446
|
}
|
|
444
447
|
});
|
|
445
|
-
var downloadCommand = new import_commander.Command().name("download").alias("get").argument("[templateId]", "Template ID to download").description("Download a template to local storage").option("-a, --all", "Download all available templates").
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
448
|
+
var downloadCommand = new import_commander.Command().name("download").alias("get").argument("[templateId]", "Template ID to download").description("Download a template to local storage").option("-a, --all", "Download all available templates").option("-r, --refresh", "Force refresh the template index cache").action(
|
|
449
|
+
async (templateId, options) => {
|
|
450
|
+
const manager = getTemplateManager();
|
|
451
|
+
if (options?.all) {
|
|
452
|
+
const spinner2 = (0, import_ora.default)("Fetching template list...").start();
|
|
453
|
+
try {
|
|
454
|
+
const templates2 = await manager.listRemoteTemplates({
|
|
455
|
+
forceRefresh: true
|
|
456
|
+
});
|
|
457
|
+
const toDownload = templates2.filter((t) => !t.isDownloaded);
|
|
458
|
+
spinner2.stop();
|
|
459
|
+
if (toDownload.length === 0) {
|
|
460
|
+
console.log(import_chalk.default.green("\u2713 All templates already downloaded"));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
console.log(
|
|
464
|
+
import_chalk.default.bold(`
|
|
459
465
|
Downloading ${toDownload.length} templates...
|
|
460
466
|
`)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
467
|
+
);
|
|
468
|
+
for (const t of toDownload) {
|
|
469
|
+
const downloadSpinner = (0, import_ora.default)(
|
|
470
|
+
`Downloading ${t.metadata.id}...`
|
|
471
|
+
).start();
|
|
472
|
+
try {
|
|
473
|
+
await manager.downloadTemplate(t.metadata.id);
|
|
474
|
+
downloadSpinner.succeed(`Downloaded ${t.metadata.id}`);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
downloadSpinner.fail(
|
|
477
|
+
`Failed: ${t.metadata.id} - ${error.message}`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
471
480
|
}
|
|
481
|
+
console.log(import_chalk.default.green("\n\u2713 Download complete\n"));
|
|
482
|
+
} catch (error) {
|
|
483
|
+
spinner2.fail("Failed to fetch templates");
|
|
484
|
+
console.error(import_chalk.default.red(error.message));
|
|
485
|
+
process.exitCode = 1;
|
|
472
486
|
}
|
|
473
|
-
|
|
474
|
-
} catch (error) {
|
|
475
|
-
spinner2.fail("Failed to fetch templates");
|
|
476
|
-
console.error(import_chalk.default.red(error.message));
|
|
477
|
-
process.exitCode = 1;
|
|
487
|
+
return;
|
|
478
488
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
489
|
+
if (!templateId) {
|
|
490
|
+
const spinner2 = (0, import_ora.default)("Fetching templates...").start();
|
|
491
|
+
try {
|
|
492
|
+
const templates2 = await manager.listRemoteTemplates({
|
|
493
|
+
forceRefresh: !!options?.refresh
|
|
494
|
+
});
|
|
495
|
+
spinner2.stop();
|
|
496
|
+
const notDownloaded = templates2.filter((t) => !t.isDownloaded);
|
|
497
|
+
if (notDownloaded.length === 0) {
|
|
498
|
+
console.log(import_chalk.default.green("\u2713 All templates already downloaded"));
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const choices = notDownloaded.map((t) => ({
|
|
502
|
+
title: t.metadata.id,
|
|
503
|
+
description: `${t.metadata.provider} - ${t.metadata.event}`,
|
|
504
|
+
value: t.metadata.id
|
|
505
|
+
}));
|
|
506
|
+
const response = await (0, import_prompts.default)({
|
|
507
|
+
type: "select",
|
|
508
|
+
name: "templateId",
|
|
509
|
+
message: "Select a template to download:",
|
|
510
|
+
choices
|
|
511
|
+
});
|
|
512
|
+
if (!response.templateId) {
|
|
513
|
+
console.log(import_chalk.default.yellow("Cancelled"));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
templateId = response.templateId;
|
|
517
|
+
} catch (error) {
|
|
518
|
+
spinner2.fail("Failed to fetch templates");
|
|
519
|
+
console.error(import_chalk.default.red(error.message));
|
|
520
|
+
process.exitCode = 1;
|
|
504
521
|
return;
|
|
505
522
|
}
|
|
506
|
-
|
|
523
|
+
}
|
|
524
|
+
const spinner = (0, import_ora.default)(`Downloading ${templateId}...`).start();
|
|
525
|
+
try {
|
|
526
|
+
const template = await manager.downloadTemplate(templateId);
|
|
527
|
+
spinner.succeed(`Downloaded ${templateId}`);
|
|
528
|
+
console.log(import_chalk.default.gray(` Saved to: ${template.filePath}`));
|
|
529
|
+
console.log(
|
|
530
|
+
import_chalk.default.gray(` Run with: better-webhook run ${templateId}
|
|
531
|
+
`)
|
|
532
|
+
);
|
|
507
533
|
} catch (error) {
|
|
508
|
-
|
|
534
|
+
spinner.fail(`Failed to download ${templateId}`);
|
|
509
535
|
console.error(import_chalk.default.red(error.message));
|
|
510
536
|
process.exitCode = 1;
|
|
511
|
-
return;
|
|
512
537
|
}
|
|
513
538
|
}
|
|
514
|
-
|
|
515
|
-
try {
|
|
516
|
-
const template = await manager.downloadTemplate(templateId);
|
|
517
|
-
spinner.succeed(`Downloaded ${templateId}`);
|
|
518
|
-
console.log(import_chalk.default.gray(` Saved to: ${template.filePath}`));
|
|
519
|
-
console.log(import_chalk.default.gray(` Run with: better-webhook run ${templateId}
|
|
520
|
-
`));
|
|
521
|
-
} catch (error) {
|
|
522
|
-
spinner.fail(`Failed to download ${templateId}`);
|
|
523
|
-
console.error(import_chalk.default.red(error.message));
|
|
524
|
-
process.exitCode = 1;
|
|
525
|
-
}
|
|
526
|
-
});
|
|
539
|
+
);
|
|
527
540
|
var localCommand = new import_commander.Command().name("local").description("List downloaded local templates").option("-p, --provider <provider>", "Filter by provider").action((options) => {
|
|
528
541
|
const manager = getTemplateManager();
|
|
529
542
|
let templates2 = manager.listLocalTemplates();
|
|
@@ -993,6 +1006,7 @@ function getSecretEnvVarName(provider) {
|
|
|
993
1006
|
stripe: "STRIPE_WEBHOOK_SECRET",
|
|
994
1007
|
shopify: "SHOPIFY_WEBHOOK_SECRET",
|
|
995
1008
|
twilio: "TWILIO_WEBHOOK_SECRET",
|
|
1009
|
+
ragie: "RAGIE_WEBHOOK_SECRET",
|
|
996
1010
|
slack: "SLACK_WEBHOOK_SECRET",
|
|
997
1011
|
linear: "LINEAR_WEBHOOK_SECRET",
|
|
998
1012
|
clerk: "CLERK_WEBHOOK_SECRET",
|
|
@@ -1187,8 +1201,13 @@ var CaptureServer = class {
|
|
|
1187
1201
|
capturesDir;
|
|
1188
1202
|
clients = /* @__PURE__ */ new Set();
|
|
1189
1203
|
captureCount = 0;
|
|
1190
|
-
|
|
1204
|
+
enableWebSocket;
|
|
1205
|
+
onCapture;
|
|
1206
|
+
constructor(options) {
|
|
1207
|
+
const capturesDir = typeof options === "string" ? options : options?.capturesDir;
|
|
1191
1208
|
this.capturesDir = capturesDir || (0, import_path2.join)((0, import_os2.homedir)(), ".better-webhook", "captures");
|
|
1209
|
+
this.enableWebSocket = typeof options === "object" ? options?.enableWebSocket !== false : true;
|
|
1210
|
+
this.onCapture = typeof options === "object" ? options?.onCapture : void 0;
|
|
1192
1211
|
if (!(0, import_fs2.existsSync)(this.capturesDir)) {
|
|
1193
1212
|
(0, import_fs2.mkdirSync)(this.capturesDir, { recursive: true });
|
|
1194
1213
|
}
|
|
@@ -1205,26 +1224,28 @@ var CaptureServer = class {
|
|
|
1205
1224
|
async start(port = 3001, host = "0.0.0.0") {
|
|
1206
1225
|
return new Promise((resolve, reject) => {
|
|
1207
1226
|
this.server = (0, import_http.createServer)((req, res) => this.handleRequest(req, res));
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
this.
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1227
|
+
if (this.enableWebSocket) {
|
|
1228
|
+
this.wss = new import_ws.WebSocketServer({ server: this.server });
|
|
1229
|
+
this.wss.on("connection", (ws) => {
|
|
1230
|
+
this.clients.add(ws);
|
|
1231
|
+
console.log("\u{1F4E1} Dashboard connected via WebSocket");
|
|
1232
|
+
ws.on("close", () => {
|
|
1233
|
+
this.clients.delete(ws);
|
|
1234
|
+
console.log("\u{1F4E1} Dashboard disconnected");
|
|
1235
|
+
});
|
|
1236
|
+
ws.on("error", (error) => {
|
|
1237
|
+
console.error("WebSocket error:", error);
|
|
1238
|
+
this.clients.delete(ws);
|
|
1239
|
+
});
|
|
1240
|
+
this.sendToClient(ws, {
|
|
1241
|
+
type: "captures_updated",
|
|
1242
|
+
payload: {
|
|
1243
|
+
captures: this.listCaptures(),
|
|
1244
|
+
count: this.captureCount
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1226
1247
|
});
|
|
1227
|
-
}
|
|
1248
|
+
}
|
|
1228
1249
|
this.server.on("error", (err) => {
|
|
1229
1250
|
if (err.code === "EADDRINUSE") {
|
|
1230
1251
|
reject(new Error(`Port ${port} is already in use`));
|
|
@@ -1242,7 +1263,9 @@ var CaptureServer = class {
|
|
|
1242
1263
|
);
|
|
1243
1264
|
console.log(` \u{1F4C1} Captures saved to: ${this.capturesDir}`);
|
|
1244
1265
|
console.log(` \u{1F4A1} Send webhooks to any path to capture them`);
|
|
1245
|
-
|
|
1266
|
+
if (this.enableWebSocket) {
|
|
1267
|
+
console.log(` \u{1F310} WebSocket available for real-time updates`);
|
|
1268
|
+
}
|
|
1246
1269
|
console.log(` \u23F9\uFE0F Press Ctrl+C to stop
|
|
1247
1270
|
`);
|
|
1248
1271
|
resolve(actualPort);
|
|
@@ -1342,13 +1365,16 @@ var CaptureServer = class {
|
|
|
1342
1365
|
console.log(
|
|
1343
1366
|
`\u{1F4E6} ${req.method} ${urlParts.pathname}${providerStr} -> ${filename}`
|
|
1344
1367
|
);
|
|
1345
|
-
this.
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1368
|
+
this.onCapture?.({ file: filename, capture: captured });
|
|
1369
|
+
if (this.enableWebSocket) {
|
|
1370
|
+
this.broadcast({
|
|
1371
|
+
type: "capture",
|
|
1372
|
+
payload: {
|
|
1373
|
+
file: filename,
|
|
1374
|
+
capture: captured
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1352
1378
|
} catch (error) {
|
|
1353
1379
|
console.error(`\u274C Failed to save capture:`, error);
|
|
1354
1380
|
}
|
|
@@ -1375,6 +1401,9 @@ var CaptureServer = class {
|
|
|
1375
1401
|
if (headers["x-github-event"] || headers["x-hub-signature-256"]) {
|
|
1376
1402
|
return "github";
|
|
1377
1403
|
}
|
|
1404
|
+
if (headers["x-ragie-delivery"]) {
|
|
1405
|
+
return "ragie";
|
|
1406
|
+
}
|
|
1378
1407
|
if (headers["x-shopify-hmac-sha256"] || headers["x-shopify-topic"]) {
|
|
1379
1408
|
return "shopify";
|
|
1380
1409
|
}
|
|
@@ -1454,8 +1483,7 @@ var CaptureServer = class {
|
|
|
1454
1483
|
return false;
|
|
1455
1484
|
}
|
|
1456
1485
|
try {
|
|
1457
|
-
|
|
1458
|
-
fs.unlinkSync((0, import_path2.join)(this.capturesDir, capture2.file));
|
|
1486
|
+
(0, import_fs2.unlinkSync)((0, import_path2.join)(this.capturesDir, capture2.file));
|
|
1459
1487
|
return true;
|
|
1460
1488
|
} catch {
|
|
1461
1489
|
return false;
|
|
@@ -2078,9 +2106,483 @@ var replay = new import_commander5.Command().name("replay").argument("[captureId
|
|
|
2078
2106
|
}
|
|
2079
2107
|
);
|
|
2080
2108
|
|
|
2109
|
+
// src/commands/dashboard.ts
|
|
2110
|
+
var import_commander6 = require("commander");
|
|
2111
|
+
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
2112
|
+
|
|
2113
|
+
// src/core/dashboard-server.ts
|
|
2114
|
+
var import_express2 = __toESM(require("express"), 1);
|
|
2115
|
+
var import_http2 = require("http");
|
|
2116
|
+
var import_ws2 = require("ws");
|
|
2117
|
+
var import_path4 = __toESM(require("path"), 1);
|
|
2118
|
+
var import_fs4 = require("fs");
|
|
2119
|
+
var import_url = require("url");
|
|
2120
|
+
|
|
2121
|
+
// src/core/dashboard-api.ts
|
|
2122
|
+
var import_express = __toESM(require("express"), 1);
|
|
2123
|
+
var import_zod2 = require("zod");
|
|
2124
|
+
function jsonError(res, status, message) {
|
|
2125
|
+
return res.status(status).json({ error: message });
|
|
2126
|
+
}
|
|
2127
|
+
function getSecretEnvVarName2(provider) {
|
|
2128
|
+
const envVarMap = {
|
|
2129
|
+
github: "GITHUB_WEBHOOK_SECRET",
|
|
2130
|
+
stripe: "STRIPE_WEBHOOK_SECRET",
|
|
2131
|
+
shopify: "SHOPIFY_WEBHOOK_SECRET",
|
|
2132
|
+
twilio: "TWILIO_WEBHOOK_SECRET",
|
|
2133
|
+
ragie: "RAGIE_WEBHOOK_SECRET",
|
|
2134
|
+
slack: "SLACK_WEBHOOK_SECRET",
|
|
2135
|
+
linear: "LINEAR_WEBHOOK_SECRET",
|
|
2136
|
+
clerk: "CLERK_WEBHOOK_SECRET",
|
|
2137
|
+
sendgrid: "SENDGRID_WEBHOOK_SECRET",
|
|
2138
|
+
discord: "DISCORD_WEBHOOK_SECRET",
|
|
2139
|
+
custom: "WEBHOOK_SECRET"
|
|
2140
|
+
};
|
|
2141
|
+
return envVarMap[provider] || "WEBHOOK_SECRET";
|
|
2142
|
+
}
|
|
2143
|
+
var ReplayBodySchema = import_zod2.z.object({
|
|
2144
|
+
captureId: import_zod2.z.string().min(1),
|
|
2145
|
+
targetUrl: import_zod2.z.string().min(1),
|
|
2146
|
+
method: HttpMethodSchema.optional(),
|
|
2147
|
+
headers: import_zod2.z.array(HeaderEntrySchema).optional()
|
|
2148
|
+
});
|
|
2149
|
+
var TemplateDownloadBodySchema = import_zod2.z.object({
|
|
2150
|
+
id: import_zod2.z.string().min(1)
|
|
2151
|
+
});
|
|
2152
|
+
var RunTemplateBodySchema = import_zod2.z.object({
|
|
2153
|
+
templateId: import_zod2.z.string().min(1),
|
|
2154
|
+
url: import_zod2.z.string().min(1),
|
|
2155
|
+
secret: import_zod2.z.string().optional(),
|
|
2156
|
+
headers: import_zod2.z.array(HeaderEntrySchema).optional()
|
|
2157
|
+
});
|
|
2158
|
+
function createDashboardApiRouter(options = {}) {
|
|
2159
|
+
const router = import_express.default.Router();
|
|
2160
|
+
const replayEngine = new ReplayEngine(options.capturesDir);
|
|
2161
|
+
const templateManager = new TemplateManager(options.templatesBaseDir);
|
|
2162
|
+
const broadcast = options.broadcast;
|
|
2163
|
+
const broadcastCaptures = () => {
|
|
2164
|
+
if (!broadcast) return;
|
|
2165
|
+
const captures2 = replayEngine.listCaptures(200);
|
|
2166
|
+
broadcast({
|
|
2167
|
+
type: "captures_updated",
|
|
2168
|
+
payload: { captures: captures2, count: captures2.length }
|
|
2169
|
+
});
|
|
2170
|
+
};
|
|
2171
|
+
const broadcastTemplates = async () => {
|
|
2172
|
+
if (!broadcast) return;
|
|
2173
|
+
const local = templateManager.listLocalTemplates();
|
|
2174
|
+
let remote = [];
|
|
2175
|
+
try {
|
|
2176
|
+
const index = await templateManager.fetchRemoteIndex(false);
|
|
2177
|
+
const localIds = new Set(local.map((t) => t.id));
|
|
2178
|
+
remote = index.templates.map((metadata) => ({
|
|
2179
|
+
metadata,
|
|
2180
|
+
isDownloaded: localIds.has(metadata.id)
|
|
2181
|
+
}));
|
|
2182
|
+
} catch {
|
|
2183
|
+
remote = [];
|
|
2184
|
+
}
|
|
2185
|
+
broadcast({
|
|
2186
|
+
type: "templates_updated",
|
|
2187
|
+
payload: { local, remote }
|
|
2188
|
+
});
|
|
2189
|
+
};
|
|
2190
|
+
router.get("/captures", (req, res) => {
|
|
2191
|
+
const limitRaw = typeof req.query.limit === "string" ? req.query.limit : "";
|
|
2192
|
+
const providerRaw = typeof req.query.provider === "string" ? req.query.provider : "";
|
|
2193
|
+
const qRaw = typeof req.query.q === "string" ? req.query.q : "";
|
|
2194
|
+
const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 50;
|
|
2195
|
+
if (!Number.isFinite(limit) || limit <= 0 || limit > 5e3) {
|
|
2196
|
+
return jsonError(res, 400, "Invalid limit");
|
|
2197
|
+
}
|
|
2198
|
+
const q = qRaw.trim();
|
|
2199
|
+
const provider = providerRaw.trim();
|
|
2200
|
+
let captures2 = q ? replayEngine.searchCaptures(q) : replayEngine.listCaptures(Math.max(limit, 1e3));
|
|
2201
|
+
if (provider) {
|
|
2202
|
+
captures2 = captures2.filter(
|
|
2203
|
+
(c) => (c.capture.provider || "").toLowerCase() === provider.toLowerCase()
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
captures2 = captures2.slice(0, limit);
|
|
2207
|
+
return res.json({ captures: captures2, count: captures2.length });
|
|
2208
|
+
});
|
|
2209
|
+
router.get("/captures/:id", (req, res) => {
|
|
2210
|
+
const id = req.params.id;
|
|
2211
|
+
if (!id) {
|
|
2212
|
+
return jsonError(res, 400, "Missing capture id");
|
|
2213
|
+
}
|
|
2214
|
+
const captureFile = replayEngine.getCapture(id);
|
|
2215
|
+
if (!captureFile) {
|
|
2216
|
+
return jsonError(res, 404, "Capture not found");
|
|
2217
|
+
}
|
|
2218
|
+
return res.json(captureFile);
|
|
2219
|
+
});
|
|
2220
|
+
router.delete("/captures/:id", (req, res) => {
|
|
2221
|
+
const id = req.params.id;
|
|
2222
|
+
if (!id) {
|
|
2223
|
+
return jsonError(res, 400, "Missing capture id");
|
|
2224
|
+
}
|
|
2225
|
+
const deleted = replayEngine.deleteCapture(id);
|
|
2226
|
+
if (!deleted) {
|
|
2227
|
+
return jsonError(res, 404, "Capture not found");
|
|
2228
|
+
}
|
|
2229
|
+
broadcastCaptures();
|
|
2230
|
+
return res.json({ success: true });
|
|
2231
|
+
});
|
|
2232
|
+
router.delete("/captures", (_req, res) => {
|
|
2233
|
+
const deleted = replayEngine.deleteAllCaptures();
|
|
2234
|
+
broadcastCaptures();
|
|
2235
|
+
return res.json({ success: true, deleted });
|
|
2236
|
+
});
|
|
2237
|
+
router.post("/replay", import_express.default.json({ limit: "5mb" }), async (req, res) => {
|
|
2238
|
+
const parsed = ReplayBodySchema.safeParse(req.body);
|
|
2239
|
+
if (!parsed.success) {
|
|
2240
|
+
return jsonError(
|
|
2241
|
+
res,
|
|
2242
|
+
400,
|
|
2243
|
+
parsed.error.issues[0]?.message || "Invalid body"
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
const { captureId, targetUrl, method, headers } = parsed.data;
|
|
2247
|
+
try {
|
|
2248
|
+
new URL(targetUrl);
|
|
2249
|
+
} catch {
|
|
2250
|
+
return jsonError(res, 400, "Invalid targetUrl");
|
|
2251
|
+
}
|
|
2252
|
+
try {
|
|
2253
|
+
const result = await replayEngine.replay(captureId, {
|
|
2254
|
+
targetUrl,
|
|
2255
|
+
method,
|
|
2256
|
+
headers
|
|
2257
|
+
});
|
|
2258
|
+
broadcast?.({
|
|
2259
|
+
type: "replay_result",
|
|
2260
|
+
payload: { captureId, targetUrl, result }
|
|
2261
|
+
});
|
|
2262
|
+
return res.json(result);
|
|
2263
|
+
} catch (error) {
|
|
2264
|
+
return jsonError(res, 400, error?.message || "Replay failed");
|
|
2265
|
+
}
|
|
2266
|
+
});
|
|
2267
|
+
router.get("/templates/local", (_req, res) => {
|
|
2268
|
+
const local = templateManager.listLocalTemplates();
|
|
2269
|
+
return res.json({ templates: local, count: local.length });
|
|
2270
|
+
});
|
|
2271
|
+
router.get("/templates/remote", async (req, res) => {
|
|
2272
|
+
const refresh = typeof req.query.refresh === "string" ? req.query.refresh === "1" || req.query.refresh.toLowerCase() === "true" : false;
|
|
2273
|
+
try {
|
|
2274
|
+
const index = await templateManager.fetchRemoteIndex(refresh);
|
|
2275
|
+
const localIds = new Set(
|
|
2276
|
+
templateManager.listLocalTemplates().map((t) => t.id)
|
|
2277
|
+
);
|
|
2278
|
+
const remote = index.templates.map((metadata) => ({
|
|
2279
|
+
metadata,
|
|
2280
|
+
isDownloaded: localIds.has(metadata.id)
|
|
2281
|
+
}));
|
|
2282
|
+
return res.json({ templates: remote, count: remote.length });
|
|
2283
|
+
} catch (error) {
|
|
2284
|
+
return jsonError(
|
|
2285
|
+
res,
|
|
2286
|
+
500,
|
|
2287
|
+
error?.message || "Failed to fetch remote templates"
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
});
|
|
2291
|
+
router.post(
|
|
2292
|
+
"/templates/download",
|
|
2293
|
+
import_express.default.json({ limit: "2mb" }),
|
|
2294
|
+
async (req, res) => {
|
|
2295
|
+
const parsed = TemplateDownloadBodySchema.safeParse(req.body);
|
|
2296
|
+
if (!parsed.success) {
|
|
2297
|
+
return jsonError(
|
|
2298
|
+
res,
|
|
2299
|
+
400,
|
|
2300
|
+
parsed.error.issues[0]?.message || "Invalid body"
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
try {
|
|
2304
|
+
const template = await templateManager.downloadTemplate(parsed.data.id);
|
|
2305
|
+
void broadcastTemplates();
|
|
2306
|
+
return res.json({ success: true, template });
|
|
2307
|
+
} catch (error) {
|
|
2308
|
+
return jsonError(res, 400, error?.message || "Download failed");
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
);
|
|
2312
|
+
router.post(
|
|
2313
|
+
"/templates/download-all",
|
|
2314
|
+
import_express.default.json({ limit: "1mb" }),
|
|
2315
|
+
async (_req, res) => {
|
|
2316
|
+
try {
|
|
2317
|
+
const index = await templateManager.fetchRemoteIndex(true);
|
|
2318
|
+
const localIds = new Set(
|
|
2319
|
+
templateManager.listLocalTemplates().map((t) => t.id)
|
|
2320
|
+
);
|
|
2321
|
+
const toDownload = index.templates.filter((t) => !localIds.has(t.id));
|
|
2322
|
+
const downloaded = [];
|
|
2323
|
+
const failed = [];
|
|
2324
|
+
for (const t of toDownload) {
|
|
2325
|
+
try {
|
|
2326
|
+
await templateManager.downloadTemplate(t.id);
|
|
2327
|
+
downloaded.push(t.id);
|
|
2328
|
+
} catch (e) {
|
|
2329
|
+
failed.push({ id: t.id, error: e?.message || "Failed" });
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return res.json({
|
|
2333
|
+
success: true,
|
|
2334
|
+
total: index.templates.length,
|
|
2335
|
+
downloaded,
|
|
2336
|
+
failed
|
|
2337
|
+
});
|
|
2338
|
+
} catch (error) {
|
|
2339
|
+
return jsonError(res, 500, error?.message || "Download-all failed");
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
);
|
|
2343
|
+
router.post("/run", import_express.default.json({ limit: "10mb" }), async (req, res) => {
|
|
2344
|
+
const parsed = RunTemplateBodySchema.safeParse(req.body);
|
|
2345
|
+
if (!parsed.success) {
|
|
2346
|
+
return jsonError(
|
|
2347
|
+
res,
|
|
2348
|
+
400,
|
|
2349
|
+
parsed.error.issues[0]?.message || "Invalid body"
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
let { templateId, url, secret, headers } = parsed.data;
|
|
2353
|
+
try {
|
|
2354
|
+
new URL(url);
|
|
2355
|
+
} catch {
|
|
2356
|
+
return jsonError(res, 400, "Invalid url");
|
|
2357
|
+
}
|
|
2358
|
+
if (templateId.startsWith("remote:")) {
|
|
2359
|
+
templateId = templateId.slice("remote:".length);
|
|
2360
|
+
try {
|
|
2361
|
+
await templateManager.downloadTemplate(templateId);
|
|
2362
|
+
} catch (error) {
|
|
2363
|
+
return jsonError(
|
|
2364
|
+
res,
|
|
2365
|
+
400,
|
|
2366
|
+
error?.message || "Failed to download template"
|
|
2367
|
+
);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
let localTemplate = templateManager.getLocalTemplate(templateId);
|
|
2371
|
+
if (!localTemplate) {
|
|
2372
|
+
try {
|
|
2373
|
+
await templateManager.downloadTemplate(templateId);
|
|
2374
|
+
localTemplate = templateManager.getLocalTemplate(templateId);
|
|
2375
|
+
} catch {
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
if (!localTemplate) {
|
|
2379
|
+
return jsonError(res, 404, "Template not found");
|
|
2380
|
+
}
|
|
2381
|
+
if (!secret && localTemplate.metadata.provider) {
|
|
2382
|
+
const envVarName = getSecretEnvVarName2(localTemplate.metadata.provider);
|
|
2383
|
+
secret = process.env[envVarName];
|
|
2384
|
+
}
|
|
2385
|
+
const safeHeaders = headers?.length ? headers : void 0;
|
|
2386
|
+
try {
|
|
2387
|
+
const result = await executeTemplate(localTemplate.template, {
|
|
2388
|
+
url,
|
|
2389
|
+
secret,
|
|
2390
|
+
headers: safeHeaders
|
|
2391
|
+
});
|
|
2392
|
+
broadcast?.({
|
|
2393
|
+
type: "replay_result",
|
|
2394
|
+
payload: { templateId, url, result }
|
|
2395
|
+
});
|
|
2396
|
+
return res.json(result);
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
return jsonError(res, 400, error?.message || "Run failed");
|
|
2399
|
+
}
|
|
2400
|
+
});
|
|
2401
|
+
return router;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// src/core/dashboard-server.ts
|
|
2405
|
+
var import_meta = {};
|
|
2406
|
+
function resolveDashboardDistDir(runtimeDir) {
|
|
2407
|
+
const candidates = [
|
|
2408
|
+
import_path4.default.resolve(runtimeDir, "..", "dashboard"),
|
|
2409
|
+
import_path4.default.resolve(runtimeDir, "..", "..", "dist", "dashboard"),
|
|
2410
|
+
import_path4.default.resolve(runtimeDir, "..", "..", "..", "dashboard", "dist")
|
|
2411
|
+
];
|
|
2412
|
+
for (const distDir of candidates) {
|
|
2413
|
+
const indexHtml = import_path4.default.join(distDir, "index.html");
|
|
2414
|
+
if ((0, import_fs4.existsSync)(indexHtml)) {
|
|
2415
|
+
return { distDir, indexHtml };
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
const details = candidates.map((p) => `- ${p}`).join("\n");
|
|
2419
|
+
throw new Error(
|
|
2420
|
+
`Dashboard UI build output not found.
|
|
2421
|
+
Looked in:
|
|
2422
|
+
${details}
|
|
2423
|
+
|
|
2424
|
+
Build it with:
|
|
2425
|
+
- pnpm --filter @better-webhook/dashboard build
|
|
2426
|
+
- pnpm --filter @better-webhook/cli build
|
|
2427
|
+
`
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2430
|
+
async function startDashboardServer(options = {}) {
|
|
2431
|
+
const app = (0, import_express2.default)();
|
|
2432
|
+
app.get("/health", (_req, res) => {
|
|
2433
|
+
res.json({ ok: true });
|
|
2434
|
+
});
|
|
2435
|
+
const clients = /* @__PURE__ */ new Set();
|
|
2436
|
+
const broadcast = (message) => {
|
|
2437
|
+
const data = JSON.stringify(message);
|
|
2438
|
+
for (const client of clients) {
|
|
2439
|
+
if (client.readyState === 1) {
|
|
2440
|
+
client.send(data);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
};
|
|
2444
|
+
app.use(
|
|
2445
|
+
"/api",
|
|
2446
|
+
createDashboardApiRouter({
|
|
2447
|
+
capturesDir: options.capturesDir,
|
|
2448
|
+
templatesBaseDir: options.templatesBaseDir,
|
|
2449
|
+
broadcast
|
|
2450
|
+
})
|
|
2451
|
+
);
|
|
2452
|
+
const host = options.host || "localhost";
|
|
2453
|
+
const port = options.port ?? 4e3;
|
|
2454
|
+
const runtimeDir = (
|
|
2455
|
+
// eslint-disable-next-line no-undef
|
|
2456
|
+
typeof __dirname !== "undefined" ? (
|
|
2457
|
+
// eslint-disable-next-line no-undef
|
|
2458
|
+
__dirname
|
|
2459
|
+
) : import_path4.default.dirname((0, import_url.fileURLToPath)(import_meta.url))
|
|
2460
|
+
);
|
|
2461
|
+
const { distDir: dashboardDistDir, indexHtml: dashboardIndexHtml } = resolveDashboardDistDir(runtimeDir);
|
|
2462
|
+
app.use(import_express2.default.static(dashboardDistDir));
|
|
2463
|
+
app.get("*", (req, res, next) => {
|
|
2464
|
+
if (req.path.startsWith("/api") || req.path === "/health") return next();
|
|
2465
|
+
res.sendFile(dashboardIndexHtml, (err) => {
|
|
2466
|
+
if (err) next();
|
|
2467
|
+
});
|
|
2468
|
+
});
|
|
2469
|
+
const server = (0, import_http2.createServer)(app);
|
|
2470
|
+
const wss = new import_ws2.WebSocketServer({ server, path: "/ws" });
|
|
2471
|
+
wss.on("connection", async (ws) => {
|
|
2472
|
+
clients.add(ws);
|
|
2473
|
+
ws.on("close", () => clients.delete(ws));
|
|
2474
|
+
ws.on("error", () => clients.delete(ws));
|
|
2475
|
+
const replayEngine = new ReplayEngine(options.capturesDir);
|
|
2476
|
+
const templateManager = new TemplateManager(options.templatesBaseDir);
|
|
2477
|
+
const captures2 = replayEngine.listCaptures(200);
|
|
2478
|
+
ws.send(
|
|
2479
|
+
JSON.stringify({
|
|
2480
|
+
type: "captures_updated",
|
|
2481
|
+
payload: { captures: captures2, count: captures2.length }
|
|
2482
|
+
})
|
|
2483
|
+
);
|
|
2484
|
+
const local = templateManager.listLocalTemplates();
|
|
2485
|
+
let remote = [];
|
|
2486
|
+
try {
|
|
2487
|
+
const index = await templateManager.fetchRemoteIndex(true);
|
|
2488
|
+
const localIds = new Set(local.map((t) => t.id));
|
|
2489
|
+
remote = index.templates.map((metadata) => ({
|
|
2490
|
+
metadata,
|
|
2491
|
+
isDownloaded: localIds.has(metadata.id)
|
|
2492
|
+
}));
|
|
2493
|
+
} catch {
|
|
2494
|
+
remote = [];
|
|
2495
|
+
}
|
|
2496
|
+
ws.send(
|
|
2497
|
+
JSON.stringify({
|
|
2498
|
+
type: "templates_updated",
|
|
2499
|
+
payload: { local, remote }
|
|
2500
|
+
})
|
|
2501
|
+
);
|
|
2502
|
+
});
|
|
2503
|
+
await new Promise((resolve, reject) => {
|
|
2504
|
+
server.listen(port, host, () => resolve());
|
|
2505
|
+
server.on("error", reject);
|
|
2506
|
+
});
|
|
2507
|
+
const url = `http://${host}:${port}`;
|
|
2508
|
+
let capture2;
|
|
2509
|
+
const shouldStartCapture = options.startCapture !== false;
|
|
2510
|
+
if (shouldStartCapture) {
|
|
2511
|
+
const captureHost = options.captureHost || "0.0.0.0";
|
|
2512
|
+
const capturePort = options.capturePort ?? 3001;
|
|
2513
|
+
const captureServer = new CaptureServer({
|
|
2514
|
+
capturesDir: options.capturesDir,
|
|
2515
|
+
enableWebSocket: false,
|
|
2516
|
+
onCapture: ({ file, capture: capture3 }) => {
|
|
2517
|
+
broadcast({
|
|
2518
|
+
type: "capture",
|
|
2519
|
+
payload: { file, capture: capture3 }
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
});
|
|
2523
|
+
const actualPort = await captureServer.start(capturePort, captureHost);
|
|
2524
|
+
capture2 = {
|
|
2525
|
+
server: captureServer,
|
|
2526
|
+
url: `http://${captureHost === "0.0.0.0" ? "localhost" : captureHost}:${actualPort}`
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
return { app, server, url, capture: capture2 };
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// src/commands/dashboard.ts
|
|
2533
|
+
var dashboard = new import_commander6.Command().name("dashboard").description("Start the local dashboard (UI + API + WebSocket) server").option("-p, --port <port>", "Port to listen on", "4000").option("-h, --host <host>", "Host to bind to", "localhost").option("--capture-port <port>", "Capture server port", "3001").option("--capture-host <host>", "Capture server host", "0.0.0.0").option("--no-capture", "Do not start the capture server").option("--captures-dir <dir>", "Override captures directory").option("--templates-dir <dir>", "Override templates base directory").action(async (options) => {
|
|
2534
|
+
const port = Number.parseInt(String(options.port), 10);
|
|
2535
|
+
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
|
2536
|
+
console.error(import_chalk6.default.red("Invalid port number"));
|
|
2537
|
+
process.exitCode = 1;
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
try {
|
|
2541
|
+
const capturePort = Number.parseInt(String(options.capturePort), 10);
|
|
2542
|
+
if (!Number.isFinite(capturePort) || capturePort < 0 || capturePort > 65535) {
|
|
2543
|
+
console.error(import_chalk6.default.red("Invalid capture port number"));
|
|
2544
|
+
process.exitCode = 1;
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
const { url, server, capture: capture2 } = await startDashboardServer({
|
|
2548
|
+
host: options.host,
|
|
2549
|
+
port,
|
|
2550
|
+
captureHost: options.captureHost,
|
|
2551
|
+
capturePort,
|
|
2552
|
+
startCapture: options.capture !== false,
|
|
2553
|
+
capturesDir: options.capturesDir,
|
|
2554
|
+
templatesBaseDir: options.templatesDir
|
|
2555
|
+
});
|
|
2556
|
+
console.log(import_chalk6.default.bold("\n\u{1F9ED} Dashboard Server\n"));
|
|
2557
|
+
console.log(import_chalk6.default.gray(` Dashboard: ${url}/`));
|
|
2558
|
+
console.log(import_chalk6.default.gray(` Health: ${url}/health`));
|
|
2559
|
+
console.log(import_chalk6.default.gray(` API Base: ${url}/api`));
|
|
2560
|
+
console.log(import_chalk6.default.gray(` WebSocket: ${url.replace("http://", "ws://")}/ws`));
|
|
2561
|
+
if (capture2) {
|
|
2562
|
+
console.log();
|
|
2563
|
+
console.log(import_chalk6.default.bold("\u{1F3A3} Capture Server"));
|
|
2564
|
+
console.log(import_chalk6.default.gray(` Capture: ${capture2.url}`));
|
|
2565
|
+
console.log(import_chalk6.default.gray(` Tip: Send webhooks to any path, e.g. ${capture2.url}/webhooks/github`));
|
|
2566
|
+
}
|
|
2567
|
+
console.log();
|
|
2568
|
+
const shutdown = async () => {
|
|
2569
|
+
if (capture2) {
|
|
2570
|
+
await capture2.server.stop();
|
|
2571
|
+
}
|
|
2572
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
2573
|
+
process.exit(0);
|
|
2574
|
+
};
|
|
2575
|
+
process.on("SIGINT", shutdown);
|
|
2576
|
+
process.on("SIGTERM", shutdown);
|
|
2577
|
+
} catch (error) {
|
|
2578
|
+
console.error(import_chalk6.default.red(`Failed to start dashboard server: ${error?.message || error}`));
|
|
2579
|
+
process.exitCode = 1;
|
|
2580
|
+
}
|
|
2581
|
+
});
|
|
2582
|
+
|
|
2081
2583
|
// src/index.ts
|
|
2082
|
-
var program = new
|
|
2584
|
+
var program = new import_commander7.Command().name("better-webhook").description(
|
|
2083
2585
|
"Modern CLI for developing, capturing, and replaying webhooks locally"
|
|
2084
2586
|
).version("2.0.0");
|
|
2085
|
-
program.addCommand(templates).addCommand(run).addCommand(capture).addCommand(captures).addCommand(replay);
|
|
2587
|
+
program.addCommand(templates).addCommand(run).addCommand(capture).addCommand(captures).addCommand(replay).addCommand(dashboard);
|
|
2086
2588
|
program.parseAsync(process.argv);
|