@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
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>better-webhook dashboard</title>
8
- <script type="module" crossorigin src="/assets/index-BSfTbn4Y.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-zDTVdss_.css">
8
+ <script type="module" crossorigin src="/assets/index-Dlqdzwyc.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-CxrRCNTh.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/dist/index.cjs CHANGED
@@ -25,7 +25,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/index.ts
27
27
  var import_commander7 = require("commander");
28
- var import_node_module = require("module");
28
+ var import_node_fs = require("fs");
29
+ var import_node_url = require("url");
29
30
 
30
31
  // src/commands/templates.ts
31
32
  var import_commander = require("commander");
@@ -123,6 +124,12 @@ var ConfigSchema = import_zod.z.object({
123
124
  // src/core/template-manager.ts
124
125
  var GITHUB_RAW_BASE = "https://raw.githubusercontent.com/endalk200/better-webhook/main";
125
126
  var TEMPLATES_INDEX_URL = `${GITHUB_RAW_BASE}/templates/templates.json`;
127
+ var TEMPLATE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
128
+ function isValidTemplateId(id) {
129
+ if (!id || id.length > 128) return false;
130
+ if (id.includes("/") || id.includes("\\") || id.includes("..")) return false;
131
+ return TEMPLATE_ID_PATTERN.test(id);
132
+ }
126
133
  var TemplateManager = class {
127
134
  baseDir;
128
135
  templatesDir;
@@ -376,6 +383,76 @@ var TemplateManager = class {
376
383
  }
377
384
  return deleted;
378
385
  }
386
+ /**
387
+ * Check if a template with the given ID already exists locally
388
+ */
389
+ templateExists(templateId) {
390
+ return this.getLocalTemplate(templateId) !== null;
391
+ }
392
+ /**
393
+ * Generate a unique template ID from provider and event
394
+ */
395
+ generateTemplateId(provider, event) {
396
+ const providerPart = provider || "custom";
397
+ const eventPart = event || "webhook";
398
+ const baseId = `${providerPart}-${eventPart}`.toLowerCase().replace(/\s+/g, "-");
399
+ if (!this.templateExists(baseId)) {
400
+ return baseId;
401
+ }
402
+ let counter = 1;
403
+ while (this.templateExists(`${baseId}-${counter}`)) {
404
+ counter++;
405
+ }
406
+ return `${baseId}-${counter}`;
407
+ }
408
+ /**
409
+ * Save a user-created template from a captured webhook
410
+ */
411
+ saveUserTemplate(template, options = {}) {
412
+ const provider = template.provider || "custom";
413
+ const event = options.event || template.event || "webhook";
414
+ const templateId = options.id || this.generateTemplateId(provider, event);
415
+ const name = options.name || templateId;
416
+ const description = options.description || template.description;
417
+ if (!isValidTemplateId(templateId)) {
418
+ throw new Error(
419
+ `Invalid template ID "${templateId}". IDs must start with alphanumeric, contain only letters, numbers, dots, underscores, and hyphens.`
420
+ );
421
+ }
422
+ if (!options.overwrite && this.templateExists(templateId)) {
423
+ throw new Error(
424
+ `Template with ID "${templateId}" already exists. Use --overwrite to replace it.`
425
+ );
426
+ }
427
+ const providerDir = (0, import_path.join)(this.templatesDir, provider);
428
+ if (!(0, import_fs.existsSync)(providerDir)) {
429
+ (0, import_fs.mkdirSync)(providerDir, { recursive: true });
430
+ }
431
+ const metadata = {
432
+ id: templateId,
433
+ name,
434
+ provider,
435
+ event,
436
+ file: `${provider}/${templateId}.json`,
437
+ description,
438
+ source: "capture",
439
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
440
+ };
441
+ const saveData = {
442
+ ...template,
443
+ provider,
444
+ event,
445
+ description,
446
+ _metadata: metadata
447
+ };
448
+ const filePath = (0, import_path.join)(providerDir, `${templateId}.json`);
449
+ (0, import_fs.writeFileSync)(filePath, JSON.stringify(saveData, null, 2));
450
+ return {
451
+ id: templateId,
452
+ filePath,
453
+ template: saveData
454
+ };
455
+ }
379
456
  };
380
457
  var instance = null;
381
458
  function getTemplateManager(baseDir) {
@@ -1543,6 +1620,7 @@ var capture = new import_commander3.Command().name("capture").description("Start
1543
1620
  var import_commander4 = require("commander");
1544
1621
  var import_chalk4 = __toESM(require("chalk"), 1);
1545
1622
  var import_prompts3 = __toESM(require("prompts"), 1);
1623
+ var import_os4 = require("os");
1546
1624
 
1547
1625
  // src/core/replay-engine.ts
1548
1626
  var import_fs3 = require("fs");
@@ -1676,15 +1754,93 @@ var ReplayEngine = class {
1676
1754
  body = capture2.rawBody;
1677
1755
  }
1678
1756
  }
1757
+ const event = options?.event || this.detectEvent(capture2);
1679
1758
  return {
1680
1759
  url: options?.url || `http://localhost:3000${capture2.path}`,
1681
1760
  method: capture2.method,
1682
1761
  headers,
1683
1762
  body,
1684
1763
  provider: capture2.provider,
1764
+ event,
1685
1765
  description: `Captured ${capture2.provider || "webhook"} at ${capture2.timestamp}`
1686
1766
  };
1687
1767
  }
1768
+ /**
1769
+ * Detect event type from captured webhook headers/body
1770
+ */
1771
+ detectEvent(capture2) {
1772
+ const headers = capture2.headers;
1773
+ const githubEvent = headers["x-github-event"];
1774
+ if (githubEvent) {
1775
+ return Array.isArray(githubEvent) ? githubEvent[0] : githubEvent;
1776
+ }
1777
+ if (capture2.provider === "stripe" && capture2.body) {
1778
+ const body = capture2.body;
1779
+ if (typeof body.type === "string") {
1780
+ return body.type;
1781
+ }
1782
+ }
1783
+ if (capture2.provider === "slack" && capture2.body) {
1784
+ const body = capture2.body;
1785
+ if (typeof body.type === "string") {
1786
+ return body.type;
1787
+ }
1788
+ const event = body.event;
1789
+ if (event && typeof event.type === "string") {
1790
+ return event.type;
1791
+ }
1792
+ }
1793
+ if (capture2.provider === "linear" && capture2.body) {
1794
+ const body = capture2.body;
1795
+ if (typeof body.type === "string") {
1796
+ return body.type;
1797
+ }
1798
+ }
1799
+ if (capture2.provider === "clerk" && capture2.body) {
1800
+ const body = capture2.body;
1801
+ if (typeof body.type === "string") {
1802
+ return body.type;
1803
+ }
1804
+ }
1805
+ if (capture2.provider === "ragie" && capture2.body) {
1806
+ const body = capture2.body;
1807
+ if (typeof body.event_type === "string") {
1808
+ return body.event_type;
1809
+ }
1810
+ }
1811
+ const shopifyTopic = headers["x-shopify-topic"];
1812
+ if (shopifyTopic) {
1813
+ return Array.isArray(shopifyTopic) ? shopifyTopic[0] : shopifyTopic;
1814
+ }
1815
+ if (capture2.provider === "sendgrid" && Array.isArray(capture2.body)) {
1816
+ const firstEvent = capture2.body[0];
1817
+ if (firstEvent && typeof firstEvent.event === "string") {
1818
+ return firstEvent.event;
1819
+ }
1820
+ }
1821
+ if (capture2.provider === "discord" && capture2.body) {
1822
+ const body = capture2.body;
1823
+ if (typeof body.type === "number") {
1824
+ return `type_${body.type}`;
1825
+ }
1826
+ }
1827
+ if (capture2.body && typeof capture2.body === "object") {
1828
+ const body = capture2.body;
1829
+ if (typeof body.type === "string") {
1830
+ return body.type;
1831
+ }
1832
+ if (typeof body.event_type === "string") {
1833
+ return body.event_type;
1834
+ }
1835
+ if (typeof body.event === "string") {
1836
+ return body.event;
1837
+ }
1838
+ if (typeof body.action === "string") {
1839
+ return body.action;
1840
+ }
1841
+ }
1842
+ return void 0;
1843
+ }
1688
1844
  /**
1689
1845
  * Get a summary of a capture
1690
1846
  */
@@ -1773,6 +1929,13 @@ function getReplayEngine(capturesDir) {
1773
1929
  }
1774
1930
 
1775
1931
  // src/commands/captures.ts
1932
+ function toRelativePath(absolutePath) {
1933
+ const home = (0, import_os4.homedir)();
1934
+ if (absolutePath.startsWith(home)) {
1935
+ return "~" + absolutePath.slice(home.length);
1936
+ }
1937
+ return absolutePath;
1938
+ }
1776
1939
  var listCommand2 = new import_commander4.Command().name("list").alias("ls").description("List captured webhooks").option("-l, --limit <limit>", "Maximum number of captures to show", "20").option("-p, --provider <provider>", "Filter by provider").action((options) => {
1777
1940
  const limit = parseInt(options.limit, 10);
1778
1941
  if (isNaN(limit) || limit <= 0) {
@@ -1981,7 +2144,92 @@ var cleanCommand2 = new import_commander4.Command().name("clean").alias("remove-
1981
2144
  console.log(import_chalk4.default.gray(` Storage: ${engine.getCapturesDir()}
1982
2145
  `));
1983
2146
  });
1984
- var captures = new import_commander4.Command().name("captures").alias("c").description("Manage captured webhooks").addCommand(listCommand2).addCommand(showCommand).addCommand(searchCommand2).addCommand(deleteCommand).addCommand(cleanCommand2);
2147
+ var saveAsTemplateCommand = new import_commander4.Command().name("save-as-template").alias("sat").argument("<captureId>", "Capture ID or partial ID").description("Save a captured webhook as a reusable template").option("--id <id>", "Template ID (auto-generated if not provided)").option("--name <name>", "Template display name").option("--event <event>", "Event type (auto-detected if not provided)").option("--description <description>", "Template description").option("--url <url>", "Default target URL for the template").option("--overwrite", "Overwrite existing template with same ID").action(
2148
+ async (captureId, options) => {
2149
+ const engine = getReplayEngine();
2150
+ const templateManager = getTemplateManager();
2151
+ const captureFile = engine.getCapture(captureId);
2152
+ if (!captureFile) {
2153
+ console.log(import_chalk4.default.red(`
2154
+ \u274C Capture not found: ${captureId}
2155
+ `));
2156
+ process.exitCode = 1;
2157
+ return;
2158
+ }
2159
+ const { capture: capture2 } = captureFile;
2160
+ console.log(import_chalk4.default.bold("\n\u{1F4CB} Capture to save as template:\n"));
2161
+ console.log(` ${import_chalk4.default.white(capture2.id.slice(0, 8))}`);
2162
+ console.log(import_chalk4.default.gray(` ${capture2.method} ${capture2.path}`));
2163
+ if (capture2.provider) {
2164
+ console.log(import_chalk4.default.gray(` Provider: ${capture2.provider}`));
2165
+ }
2166
+ console.log();
2167
+ const template = engine.captureToTemplate(captureId, {
2168
+ url: options.url,
2169
+ event: options.event
2170
+ });
2171
+ let templateId = options.id;
2172
+ if (!templateId) {
2173
+ const suggestedId = `${capture2.provider || "custom"}-${template.event || "webhook"}`.toLowerCase().replace(/\s+/g, "-");
2174
+ const response = await (0, import_prompts3.default)({
2175
+ type: "text",
2176
+ name: "templateId",
2177
+ message: "Template ID:",
2178
+ initial: suggestedId,
2179
+ validate: (value) => value.trim().length > 0 || "Template ID is required"
2180
+ });
2181
+ if (!response.templateId) {
2182
+ console.log(import_chalk4.default.yellow("Cancelled"));
2183
+ return;
2184
+ }
2185
+ templateId = response.templateId;
2186
+ }
2187
+ if (!options.overwrite && templateId && templateManager.templateExists(templateId)) {
2188
+ const response = await (0, import_prompts3.default)({
2189
+ type: "confirm",
2190
+ name: "overwrite",
2191
+ message: `Template "${templateId}" already exists. Overwrite?`,
2192
+ initial: false
2193
+ });
2194
+ if (!response.overwrite) {
2195
+ console.log(import_chalk4.default.yellow("Cancelled"));
2196
+ return;
2197
+ }
2198
+ options.overwrite = true;
2199
+ }
2200
+ try {
2201
+ const result = templateManager.saveUserTemplate(template, {
2202
+ id: templateId,
2203
+ name: options.name,
2204
+ event: options.event || template.event,
2205
+ description: options.description,
2206
+ overwrite: options.overwrite
2207
+ });
2208
+ console.log(import_chalk4.default.green(`
2209
+ \u2713 Saved template: ${result.id}`));
2210
+ console.log(import_chalk4.default.gray(` File: ${toRelativePath(result.filePath)}`));
2211
+ console.log(import_chalk4.default.gray(` Provider: ${template.provider || "custom"}`));
2212
+ if (template.event) {
2213
+ console.log(import_chalk4.default.gray(` Event: ${template.event}`));
2214
+ }
2215
+ console.log();
2216
+ console.log(import_chalk4.default.gray(" Run it with:"));
2217
+ console.log(
2218
+ import_chalk4.default.cyan(
2219
+ ` better-webhook run ${result.id} --url http://localhost:3000/webhooks
2220
+ `
2221
+ )
2222
+ );
2223
+ } catch (error) {
2224
+ const message = error instanceof Error ? error.message : "Failed to save template";
2225
+ console.log(import_chalk4.default.red(`
2226
+ \u274C ${message}
2227
+ `));
2228
+ process.exitCode = 1;
2229
+ }
2230
+ }
2231
+ );
2232
+ var captures = new import_commander4.Command().name("captures").alias("c").description("Manage captured webhooks").addCommand(listCommand2).addCommand(showCommand).addCommand(searchCommand2).addCommand(deleteCommand).addCommand(cleanCommand2).addCommand(saveAsTemplateCommand);
1985
2233
 
1986
2234
  // src/commands/replay.ts
1987
2235
  var import_commander5 = require("commander");
@@ -2168,12 +2416,28 @@ var ReplayBodySchema = import_zod2.z.object({
2168
2416
  var TemplateDownloadBodySchema = import_zod2.z.object({
2169
2417
  id: import_zod2.z.string().min(1)
2170
2418
  });
2419
+ var TemplateIdSchema = import_zod2.z.string().regex(
2420
+ /^[a-z0-9][a-z0-9._-]*$/i,
2421
+ "ID must start with alphanumeric and contain only letters, numbers, dots, underscores, and hyphens"
2422
+ ).max(128, "ID must be 128 characters or less").refine(
2423
+ (val) => !val.includes("/") && !val.includes("\\") && !val.includes(".."),
2424
+ "ID cannot contain path separators or parent directory references"
2425
+ );
2171
2426
  var RunTemplateBodySchema = import_zod2.z.object({
2172
2427
  templateId: import_zod2.z.string().min(1),
2173
2428
  url: import_zod2.z.string().min(1),
2174
2429
  secret: import_zod2.z.string().optional(),
2175
2430
  headers: import_zod2.z.array(HeaderEntrySchema).optional()
2176
2431
  });
2432
+ var SaveAsTemplateBodySchema = import_zod2.z.object({
2433
+ captureId: import_zod2.z.string().min(1),
2434
+ id: TemplateIdSchema.optional(),
2435
+ name: import_zod2.z.string().optional(),
2436
+ event: import_zod2.z.string().optional(),
2437
+ description: import_zod2.z.string().optional(),
2438
+ url: import_zod2.z.string().optional(),
2439
+ overwrite: import_zod2.z.boolean().optional()
2440
+ });
2177
2441
  function createDashboardApiRouter(options = {}) {
2178
2442
  const router = import_express.default.Router();
2179
2443
  const replayEngine = new ReplayEngine(options.capturesDir);
@@ -2189,22 +2453,26 @@ function createDashboardApiRouter(options = {}) {
2189
2453
  };
2190
2454
  const broadcastTemplates = async () => {
2191
2455
  if (!broadcast) return;
2192
- const local = templateManager.listLocalTemplates();
2193
- let remote = [];
2194
2456
  try {
2195
- const index = await templateManager.fetchRemoteIndex(false);
2196
- const localIds = new Set(local.map((t) => t.id));
2197
- remote = index.templates.map((metadata) => ({
2198
- metadata,
2199
- isDownloaded: localIds.has(metadata.id)
2200
- }));
2201
- } catch {
2202
- remote = [];
2457
+ const local = templateManager.listLocalTemplates();
2458
+ let remote = [];
2459
+ try {
2460
+ const index = await templateManager.fetchRemoteIndex(false);
2461
+ const localIds = new Set(local.map((t) => t.id));
2462
+ remote = index.templates.map((metadata) => ({
2463
+ metadata,
2464
+ isDownloaded: localIds.has(metadata.id)
2465
+ }));
2466
+ } catch {
2467
+ remote = [];
2468
+ }
2469
+ broadcast({
2470
+ type: "templates_updated",
2471
+ payload: { local, remote }
2472
+ });
2473
+ } catch (error) {
2474
+ console.error("[dashboard-api] Failed to broadcast templates:", error);
2203
2475
  }
2204
- broadcast({
2205
- type: "templates_updated",
2206
- payload: { local, remote }
2207
- });
2208
2476
  };
2209
2477
  router.get("/captures", (req, res) => {
2210
2478
  const limitRaw = typeof req.query.limit === "string" ? req.query.limit : "";
@@ -2417,6 +2685,54 @@ function createDashboardApiRouter(options = {}) {
2417
2685
  return jsonError(res, 400, error?.message || "Run failed");
2418
2686
  }
2419
2687
  });
2688
+ router.post(
2689
+ "/templates/from-capture",
2690
+ import_express.default.json({ limit: "5mb" }),
2691
+ async (req, res) => {
2692
+ const parsed = SaveAsTemplateBodySchema.safeParse(req.body);
2693
+ if (!parsed.success) {
2694
+ return jsonError(
2695
+ res,
2696
+ 400,
2697
+ parsed.error.issues[0]?.message || "Invalid body"
2698
+ );
2699
+ }
2700
+ const { captureId, id, name, event, description, url, overwrite } = parsed.data;
2701
+ if (url !== void 0) {
2702
+ try {
2703
+ new URL(url);
2704
+ } catch {
2705
+ return jsonError(res, 400, "Invalid url");
2706
+ }
2707
+ }
2708
+ const captureFile = replayEngine.getCapture(captureId);
2709
+ if (!captureFile) {
2710
+ return jsonError(res, 404, "Capture not found");
2711
+ }
2712
+ const template = replayEngine.captureToTemplate(captureId, {
2713
+ url,
2714
+ event
2715
+ });
2716
+ try {
2717
+ const result = templateManager.saveUserTemplate(template, {
2718
+ id,
2719
+ name,
2720
+ event: event || template.event,
2721
+ description,
2722
+ overwrite
2723
+ });
2724
+ void broadcastTemplates();
2725
+ return res.json({
2726
+ success: true,
2727
+ id: result.id,
2728
+ filePath: result.filePath,
2729
+ template: result.template
2730
+ });
2731
+ } catch (error) {
2732
+ return jsonError(res, 400, error?.message || "Failed to save template");
2733
+ }
2734
+ }
2735
+ );
2420
2736
  return router;
2421
2737
  }
2422
2738
 
@@ -2475,13 +2791,10 @@ async function startDashboardServer(options = {}) {
2475
2791
  );
2476
2792
  const host = options.host || "localhost";
2477
2793
  const port = options.port ?? 4e3;
2478
- const runtimeDir = (
2794
+ const runtimeDir = typeof __dirname !== "undefined" ? (
2479
2795
  // eslint-disable-next-line no-undef
2480
- typeof __dirname !== "undefined" ? (
2481
- // eslint-disable-next-line no-undef
2482
- __dirname
2483
- ) : import_path4.default.dirname((0, import_url.fileURLToPath)(import_meta.url))
2484
- );
2796
+ __dirname
2797
+ ) : import_path4.default.dirname((0, import_url.fileURLToPath)(import_meta.url));
2485
2798
  const { distDir: dashboardDistDir, indexHtml: dashboardIndexHtml } = resolveDashboardDistDir(runtimeDir);
2486
2799
  app.use(import_express2.default.static(dashboardDistDir));
2487
2800
  app.get("*", (req, res, next) => {
@@ -2616,8 +2929,12 @@ var dashboard = new import_commander6.Command().name("dashboard").description("S
2616
2929
 
2617
2930
  // src/index.ts
2618
2931
  var import_meta2 = {};
2619
- var require2 = (0, import_node_module.createRequire)(import_meta2.url);
2620
- var packageJson = require2("../package.json");
2932
+ var packageJsonPath = (0, import_node_url.fileURLToPath)(
2933
+ new URL("../package.json", import_meta2.url)
2934
+ );
2935
+ var packageJson = JSON.parse(
2936
+ (0, import_node_fs.readFileSync)(packageJsonPath, { encoding: "utf8" })
2937
+ );
2621
2938
  var program = new import_commander7.Command().name("better-webhook").description(
2622
2939
  "Modern CLI for developing, capturing, and replaying webhooks locally"
2623
2940
  ).version(packageJson.version);