@better-webhook/cli 0.3.1 → 3.1.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 +519 -180
- package/dist/index.cjs +2331 -659
- package/dist/index.d.cts +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +2351 -663
- package/package.json +19 -6
package/dist/index.cjs
CHANGED
|
@@ -24,20 +24,23 @@ 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
|
-
// src/commands/
|
|
29
|
+
// src/commands/templates.ts
|
|
30
30
|
var import_commander = require("commander");
|
|
31
|
-
var
|
|
32
|
-
var
|
|
31
|
+
var import_ora = __toESM(require("ora"), 1);
|
|
32
|
+
var import_prompts = __toESM(require("prompts"), 1);
|
|
33
|
+
var import_chalk = __toESM(require("chalk"), 1);
|
|
33
34
|
|
|
34
|
-
// src/
|
|
35
|
+
// src/core/template-manager.ts
|
|
36
|
+
var import_undici = require("undici");
|
|
35
37
|
var import_fs = require("fs");
|
|
36
38
|
var import_path = require("path");
|
|
39
|
+
var import_os = require("os");
|
|
37
40
|
|
|
38
|
-
// src/
|
|
41
|
+
// src/types/index.ts
|
|
39
42
|
var import_zod = require("zod");
|
|
40
|
-
var
|
|
43
|
+
var HttpMethodSchema = import_zod.z.enum([
|
|
41
44
|
"GET",
|
|
42
45
|
"POST",
|
|
43
46
|
"PUT",
|
|
@@ -46,459 +49,1261 @@ var httpMethodSchema = import_zod.z.enum([
|
|
|
46
49
|
"HEAD",
|
|
47
50
|
"OPTIONS"
|
|
48
51
|
]);
|
|
49
|
-
var
|
|
50
|
-
key: import_zod.z.string().min(1,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
var HeaderEntrySchema = import_zod.z.object({
|
|
53
|
+
key: import_zod.z.string().min(1),
|
|
54
|
+
value: import_zod.z.string()
|
|
55
|
+
});
|
|
56
|
+
var WebhookProviderSchema = import_zod.z.enum([
|
|
57
|
+
"stripe",
|
|
58
|
+
"github",
|
|
59
|
+
"shopify",
|
|
60
|
+
"twilio",
|
|
61
|
+
"ragie",
|
|
62
|
+
"sendgrid",
|
|
63
|
+
"slack",
|
|
64
|
+
"discord",
|
|
65
|
+
"linear",
|
|
66
|
+
"clerk",
|
|
67
|
+
"custom"
|
|
68
|
+
]);
|
|
69
|
+
var TemplateMetadataSchema = import_zod.z.object({
|
|
70
|
+
id: import_zod.z.string(),
|
|
71
|
+
name: import_zod.z.string(),
|
|
72
|
+
description: import_zod.z.string().optional(),
|
|
73
|
+
provider: WebhookProviderSchema,
|
|
74
|
+
event: import_zod.z.string(),
|
|
75
|
+
file: import_zod.z.string(),
|
|
76
|
+
version: import_zod.z.string().optional(),
|
|
77
|
+
docsUrl: import_zod.z.string().url().optional()
|
|
78
|
+
});
|
|
79
|
+
var TemplatesIndexSchema = import_zod.z.object({
|
|
80
|
+
version: import_zod.z.string(),
|
|
81
|
+
templates: import_zod.z.array(TemplateMetadataSchema)
|
|
82
|
+
});
|
|
83
|
+
var WebhookTemplateSchema = import_zod.z.object({
|
|
84
|
+
url: import_zod.z.string().url().optional(),
|
|
85
|
+
method: HttpMethodSchema.default("POST"),
|
|
86
|
+
headers: import_zod.z.array(HeaderEntrySchema).default([]),
|
|
87
|
+
body: import_zod.z.any().optional(),
|
|
88
|
+
provider: WebhookProviderSchema.optional(),
|
|
89
|
+
event: import_zod.z.string().optional(),
|
|
90
|
+
description: import_zod.z.string().optional()
|
|
91
|
+
});
|
|
92
|
+
var CapturedWebhookSchema = import_zod.z.object({
|
|
93
|
+
id: import_zod.z.string(),
|
|
94
|
+
timestamp: import_zod.z.string(),
|
|
95
|
+
method: HttpMethodSchema,
|
|
96
|
+
url: import_zod.z.string(),
|
|
97
|
+
path: import_zod.z.string(),
|
|
98
|
+
headers: import_zod.z.record(import_zod.z.union([import_zod.z.string(), import_zod.z.array(import_zod.z.string())])),
|
|
99
|
+
body: import_zod.z.any().optional(),
|
|
100
|
+
rawBody: import_zod.z.string(),
|
|
101
|
+
query: import_zod.z.record(import_zod.z.union([import_zod.z.string(), import_zod.z.array(import_zod.z.string())])),
|
|
102
|
+
provider: WebhookProviderSchema.optional(),
|
|
103
|
+
contentType: import_zod.z.string().optional(),
|
|
104
|
+
contentLength: import_zod.z.number().optional()
|
|
105
|
+
});
|
|
106
|
+
var ConfigSchema = import_zod.z.object({
|
|
107
|
+
version: import_zod.z.string().default("2.0.0"),
|
|
108
|
+
templatesDir: import_zod.z.string().optional(),
|
|
109
|
+
capturesDir: import_zod.z.string().optional(),
|
|
110
|
+
defaultTargetUrl: import_zod.z.string().url().optional(),
|
|
111
|
+
secrets: import_zod.z.record(import_zod.z.string()).optional().describe("Provider secrets for signature generation"),
|
|
112
|
+
dashboard: import_zod.z.object({
|
|
113
|
+
port: import_zod.z.number().default(4e3),
|
|
114
|
+
host: import_zod.z.string().default("localhost")
|
|
115
|
+
}).optional(),
|
|
116
|
+
capture: import_zod.z.object({
|
|
117
|
+
port: import_zod.z.number().default(3001),
|
|
118
|
+
host: import_zod.z.string().default("0.0.0.0")
|
|
119
|
+
}).optional()
|
|
54
120
|
});
|
|
55
|
-
var webhookSchema = import_zod.z.object({
|
|
56
|
-
url: import_zod.z.string().url("Invalid URL"),
|
|
57
|
-
method: httpMethodSchema.default("POST"),
|
|
58
|
-
headers: import_zod.z.array(headerEntrySchema).default([]),
|
|
59
|
-
body: import_zod.z.any().optional()
|
|
60
|
-
// could be anything JSON-serializable
|
|
61
|
-
}).strict();
|
|
62
|
-
function validateWebhookJSON(raw, source) {
|
|
63
|
-
const parsed = webhookSchema.safeParse(raw);
|
|
64
|
-
if (!parsed.success) {
|
|
65
|
-
const issues = parsed.error.issues.map((i) => `- ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
66
|
-
throw new Error(`Invalid webhook definition in ${source}:
|
|
67
|
-
${issues}`);
|
|
68
|
-
}
|
|
69
|
-
return parsed.data;
|
|
70
|
-
}
|
|
71
121
|
|
|
72
|
-
// src/
|
|
73
|
-
var
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
122
|
+
// src/core/template-manager.ts
|
|
123
|
+
var GITHUB_RAW_BASE = "https://raw.githubusercontent.com/endalk200/better-webhook/main";
|
|
124
|
+
var TEMPLATES_INDEX_URL = `${GITHUB_RAW_BASE}/templates/templates.json`;
|
|
125
|
+
var TemplateManager = class {
|
|
126
|
+
baseDir;
|
|
127
|
+
templatesDir;
|
|
128
|
+
cacheFile;
|
|
129
|
+
indexCache = null;
|
|
130
|
+
constructor(baseDir) {
|
|
131
|
+
this.baseDir = baseDir || (0, import_path.join)((0, import_os.homedir)(), ".better-webhook");
|
|
132
|
+
this.templatesDir = (0, import_path.join)(this.baseDir, "templates");
|
|
133
|
+
this.cacheFile = (0, import_path.join)(this.baseDir, "templates-cache.json");
|
|
134
|
+
if (!(0, import_fs.existsSync)(this.baseDir)) {
|
|
135
|
+
(0, import_fs.mkdirSync)(this.baseDir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
if (!(0, import_fs.existsSync)(this.templatesDir)) {
|
|
138
|
+
(0, import_fs.mkdirSync)(this.templatesDir, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get the templates directory path
|
|
143
|
+
*/
|
|
144
|
+
getTemplatesDir() {
|
|
145
|
+
return this.templatesDir;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Fetch templates index from GitHub
|
|
149
|
+
*/
|
|
150
|
+
async fetchRemoteIndex(forceRefresh = false) {
|
|
151
|
+
if (!forceRefresh && this.indexCache) {
|
|
152
|
+
return this.indexCache;
|
|
153
|
+
}
|
|
154
|
+
if (!forceRefresh && (0, import_fs.existsSync)(this.cacheFile)) {
|
|
155
|
+
try {
|
|
156
|
+
const cached = JSON.parse((0, import_fs.readFileSync)(this.cacheFile, "utf-8"));
|
|
157
|
+
const cacheAge = Date.now() - (cached.cachedAt || 0);
|
|
158
|
+
if (cacheAge < 36e5) {
|
|
159
|
+
this.indexCache = cached.index;
|
|
160
|
+
return cached.index;
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
91
165
|
try {
|
|
92
|
-
|
|
166
|
+
const { statusCode, body } = await (0, import_undici.request)(TEMPLATES_INDEX_URL);
|
|
167
|
+
if (statusCode !== 200) {
|
|
168
|
+
throw new Error(`HTTP ${statusCode}`);
|
|
169
|
+
}
|
|
170
|
+
const text = await body.text();
|
|
171
|
+
const json = JSON.parse(text);
|
|
172
|
+
const index = TemplatesIndexSchema.parse(json);
|
|
173
|
+
this.indexCache = index;
|
|
174
|
+
(0, import_fs.writeFileSync)(
|
|
175
|
+
this.cacheFile,
|
|
176
|
+
JSON.stringify({ index, cachedAt: Date.now() }, null, 2)
|
|
177
|
+
);
|
|
178
|
+
return index;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if ((0, import_fs.existsSync)(this.cacheFile)) {
|
|
181
|
+
try {
|
|
182
|
+
const cached = JSON.parse((0, import_fs.readFileSync)(this.cacheFile, "utf-8"));
|
|
183
|
+
if (cached.index) {
|
|
184
|
+
this.indexCache = cached.index;
|
|
185
|
+
return cached.index;
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`Failed to fetch templates index: ${error.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* List all remote templates
|
|
195
|
+
*/
|
|
196
|
+
async listRemoteTemplates(options) {
|
|
197
|
+
const index = await this.fetchRemoteIndex(!!options?.forceRefresh);
|
|
198
|
+
const localIds = new Set(this.listLocalTemplates().map((t) => t.id));
|
|
199
|
+
return index.templates.map((metadata) => ({
|
|
200
|
+
metadata,
|
|
201
|
+
isDownloaded: localIds.has(metadata.id)
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Download a template by ID
|
|
206
|
+
*/
|
|
207
|
+
async downloadTemplate(templateId) {
|
|
208
|
+
const index = await this.fetchRemoteIndex();
|
|
209
|
+
const templateMeta = index.templates.find((t) => t.id === templateId);
|
|
210
|
+
if (!templateMeta) {
|
|
211
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
212
|
+
}
|
|
213
|
+
const templateUrl = `${GITHUB_RAW_BASE}/templates/${templateMeta.file}`;
|
|
214
|
+
try {
|
|
215
|
+
const { statusCode, body } = await (0, import_undici.request)(templateUrl);
|
|
216
|
+
if (statusCode !== 200) {
|
|
217
|
+
throw new Error(`HTTP ${statusCode}`);
|
|
218
|
+
}
|
|
219
|
+
const text = await body.text();
|
|
220
|
+
const json = JSON.parse(text);
|
|
221
|
+
const template = WebhookTemplateSchema.parse(json);
|
|
222
|
+
const providerDir = (0, import_path.join)(this.templatesDir, templateMeta.provider);
|
|
223
|
+
if (!(0, import_fs.existsSync)(providerDir)) {
|
|
224
|
+
(0, import_fs.mkdirSync)(providerDir, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
const fileName = `${templateId}.json`;
|
|
227
|
+
const filePath = (0, import_path.join)(providerDir, fileName);
|
|
228
|
+
const localTemplate = {
|
|
229
|
+
id: templateId,
|
|
230
|
+
metadata: templateMeta,
|
|
231
|
+
template,
|
|
232
|
+
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
233
|
+
filePath
|
|
234
|
+
};
|
|
235
|
+
const saveData = {
|
|
236
|
+
...template,
|
|
237
|
+
_metadata: {
|
|
238
|
+
...templateMeta,
|
|
239
|
+
downloadedAt: localTemplate.downloadedAt
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
(0, import_fs.writeFileSync)(filePath, JSON.stringify(saveData, null, 2));
|
|
243
|
+
return localTemplate;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Failed to download template ${templateId}: ${error.message}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* List all downloaded local templates
|
|
252
|
+
*/
|
|
253
|
+
listLocalTemplates() {
|
|
254
|
+
const templates2 = [];
|
|
255
|
+
if (!(0, import_fs.existsSync)(this.templatesDir)) {
|
|
256
|
+
return templates2;
|
|
257
|
+
}
|
|
258
|
+
const scanDir = (dir) => {
|
|
259
|
+
const entries = (0, import_fs.readdirSync)(dir, { withFileTypes: true });
|
|
260
|
+
for (const entry of entries) {
|
|
261
|
+
const fullPath = (0, import_path.join)(dir, entry.name);
|
|
262
|
+
if (entry.isDirectory()) {
|
|
263
|
+
scanDir(fullPath);
|
|
264
|
+
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
265
|
+
try {
|
|
266
|
+
const content = JSON.parse((0, import_fs.readFileSync)(fullPath, "utf-8"));
|
|
267
|
+
const metadata = content._metadata;
|
|
268
|
+
if (metadata) {
|
|
269
|
+
const { _metadata, ...templateData } = content;
|
|
270
|
+
templates2.push({
|
|
271
|
+
id: metadata.id,
|
|
272
|
+
metadata,
|
|
273
|
+
template: templateData,
|
|
274
|
+
downloadedAt: metadata.downloadedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
275
|
+
filePath: fullPath
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
const id = (0, import_path.basename)(entry.name, ".json");
|
|
279
|
+
templates2.push({
|
|
280
|
+
id,
|
|
281
|
+
metadata: {
|
|
282
|
+
id,
|
|
283
|
+
name: id,
|
|
284
|
+
provider: "custom",
|
|
285
|
+
event: "unknown",
|
|
286
|
+
file: entry.name
|
|
287
|
+
},
|
|
288
|
+
template: content,
|
|
289
|
+
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
290
|
+
filePath: fullPath
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
scanDir(this.templatesDir);
|
|
299
|
+
return templates2;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Get a specific local template by ID
|
|
303
|
+
*/
|
|
304
|
+
getLocalTemplate(templateId) {
|
|
305
|
+
const templates2 = this.listLocalTemplates();
|
|
306
|
+
return templates2.find((t) => t.id === templateId) || null;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Delete a local template
|
|
310
|
+
*/
|
|
311
|
+
deleteLocalTemplate(templateId) {
|
|
312
|
+
const template = this.getLocalTemplate(templateId);
|
|
313
|
+
if (!template) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
(0, import_fs.unlinkSync)(template.filePath);
|
|
318
|
+
return true;
|
|
93
319
|
} catch {
|
|
320
|
+
return false;
|
|
94
321
|
}
|
|
95
322
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
323
|
+
/**
|
|
324
|
+
* Search templates by name, provider, or event
|
|
325
|
+
*/
|
|
326
|
+
async searchTemplates(query) {
|
|
327
|
+
const queryLower = query.toLowerCase();
|
|
328
|
+
const remote = await this.listRemoteTemplates();
|
|
329
|
+
const local = this.listLocalTemplates();
|
|
330
|
+
const matchesMeta = (meta) => {
|
|
331
|
+
return meta.id.toLowerCase().includes(queryLower) || meta.name.toLowerCase().includes(queryLower) || meta.provider.toLowerCase().includes(queryLower) || meta.event.toLowerCase().includes(queryLower) || (meta.description?.toLowerCase().includes(queryLower) ?? false);
|
|
332
|
+
};
|
|
333
|
+
return {
|
|
334
|
+
remote: remote.filter((t) => matchesMeta(t.metadata)),
|
|
335
|
+
local: local.filter((t) => matchesMeta(t.metadata))
|
|
336
|
+
};
|
|
99
337
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// src/utils/index.ts
|
|
109
|
-
function findWebhooksDir(cwd) {
|
|
110
|
-
return (0, import_path.resolve)(cwd, ".webhooks");
|
|
111
|
-
}
|
|
112
|
-
function listWebhookFiles(dir) {
|
|
113
|
-
return listJsonFiles(dir);
|
|
114
|
-
}
|
|
115
|
-
function findCapturesDir(cwd) {
|
|
116
|
-
return (0, import_path.resolve)(cwd, ".webhook-captures");
|
|
117
|
-
}
|
|
118
|
-
function listJsonFiles(dir) {
|
|
119
|
-
try {
|
|
120
|
-
const entries = (0, import_fs.readdirSync)(dir);
|
|
121
|
-
return entries.filter(
|
|
122
|
-
(e) => (0, import_fs.statSync)((0, import_path.join)(dir, e)).isFile() && (0, import_path.extname)(e) === ".json"
|
|
123
|
-
);
|
|
124
|
-
} catch {
|
|
125
|
-
return [];
|
|
338
|
+
/**
|
|
339
|
+
* Clear the templates cache
|
|
340
|
+
*/
|
|
341
|
+
clearCache() {
|
|
342
|
+
this.indexCache = null;
|
|
343
|
+
if ((0, import_fs.existsSync)(this.cacheFile)) {
|
|
344
|
+
(0, import_fs.unlinkSync)(this.cacheFile);
|
|
345
|
+
}
|
|
126
346
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Delete all local templates
|
|
349
|
+
* @returns Number of templates deleted
|
|
350
|
+
*/
|
|
351
|
+
deleteAllLocalTemplates() {
|
|
352
|
+
const templates2 = this.listLocalTemplates();
|
|
353
|
+
let deleted = 0;
|
|
354
|
+
for (const template of templates2) {
|
|
355
|
+
try {
|
|
356
|
+
(0, import_fs.unlinkSync)(template.filePath);
|
|
357
|
+
deleted++;
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if ((0, import_fs.existsSync)(this.templatesDir)) {
|
|
362
|
+
const entries = (0, import_fs.readdirSync)(this.templatesDir, { withFileTypes: true });
|
|
363
|
+
for (const entry of entries) {
|
|
364
|
+
if (entry.isDirectory()) {
|
|
365
|
+
const dirPath = (0, import_path.join)(this.templatesDir, entry.name);
|
|
366
|
+
try {
|
|
367
|
+
const contents = (0, import_fs.readdirSync)(dirPath);
|
|
368
|
+
if (contents.length === 0) {
|
|
369
|
+
(0, import_fs.rmdirSync)(dirPath);
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return deleted;
|
|
134
377
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
378
|
+
};
|
|
379
|
+
var instance = null;
|
|
380
|
+
function getTemplateManager(baseDir) {
|
|
381
|
+
if (!instance) {
|
|
382
|
+
instance = new TemplateManager(baseDir);
|
|
140
383
|
}
|
|
141
|
-
return
|
|
384
|
+
return instance;
|
|
142
385
|
}
|
|
143
386
|
|
|
144
|
-
// src/commands/
|
|
145
|
-
var
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
var TEMPLATES = {
|
|
151
|
-
"stripe-invoice.payment_succeeded": "templates/stripe-invoice.payment_succeeded.json"
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
// src/commands/webhooks.ts
|
|
155
|
-
var import_prompts = __toESM(require("prompts"), 1);
|
|
156
|
-
var import_ora = __toESM(require("ora"), 1);
|
|
157
|
-
function statExists(p) {
|
|
387
|
+
// src/commands/templates.ts
|
|
388
|
+
var listCommand = new import_commander.Command().name("list").alias("ls").description("List available remote templates from the repository").option(
|
|
389
|
+
"-p, --provider <provider>",
|
|
390
|
+
"Filter by provider (stripe, github, etc.)"
|
|
391
|
+
).option("-r, --refresh", "Force refresh the template index cache").action(async (options) => {
|
|
392
|
+
const spinner = (0, import_ora.default)("Fetching remote templates...").start();
|
|
158
393
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
var listCommand = new import_commander.Command().name("list").description("List available webhook JSON definitions in .webhooks directory").action(() => {
|
|
166
|
-
const cwd = process.cwd();
|
|
167
|
-
const dir = findWebhooksDir(cwd);
|
|
168
|
-
const files = listWebhookFiles(dir);
|
|
169
|
-
if (!files.length) {
|
|
170
|
-
console.log("\u{1F4ED} No webhook definitions found in .webhooks directory.");
|
|
171
|
-
console.log("\u{1F4A1} Create webhook files or download templates with:");
|
|
172
|
-
console.log(" better-webhook webhooks download --all");
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
console.log("Available webhook definitions:");
|
|
176
|
-
files.forEach((f) => console.log(` \u2022 ${(0, import_path2.basename)(f, ".json")}`));
|
|
177
|
-
});
|
|
178
|
-
var runCommand = new import_commander.Command().name("run").argument(
|
|
179
|
-
"[nameOrPath]",
|
|
180
|
-
"Webhook name (in .webhooks) or path to JSON file (optional - will prompt if not provided)"
|
|
181
|
-
).description(
|
|
182
|
-
"Run a webhook by name (in .webhooks) or by providing a path to a JSON file"
|
|
183
|
-
).option("-u, --url <url>", "Override destination URL").option("-m, --method <method>", "Override HTTP method").action(async (nameOrPath, options = {}) => {
|
|
184
|
-
const cwd = process.cwd();
|
|
185
|
-
const webhooksDir = findWebhooksDir(cwd);
|
|
186
|
-
let selectedNameOrPath = nameOrPath;
|
|
187
|
-
if (!selectedNameOrPath) {
|
|
188
|
-
const spinner = (0, import_ora.default)("Loading available webhooks...").start();
|
|
189
|
-
const localFiles = listWebhookFiles(webhooksDir);
|
|
190
|
-
const templates = Object.keys(TEMPLATES);
|
|
394
|
+
const manager = getTemplateManager();
|
|
395
|
+
const templates2 = await manager.listRemoteTemplates({
|
|
396
|
+
forceRefresh: !!options.refresh
|
|
397
|
+
});
|
|
191
398
|
spinner.stop();
|
|
192
|
-
if (
|
|
193
|
-
console.log("\u{1F4ED} No
|
|
194
|
-
console.log(
|
|
195
|
-
"\u{1F4A1} Create webhook files in .webhooks directory or download templates with:"
|
|
196
|
-
);
|
|
197
|
-
console.log(" better-webhook webhooks download --all");
|
|
399
|
+
if (templates2.length === 0) {
|
|
400
|
+
console.log(import_chalk.default.yellow("\u{1F4ED} No remote templates found."));
|
|
198
401
|
return;
|
|
199
402
|
}
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
title: "--- Local Webhooks (.webhooks) ---",
|
|
205
|
-
description: "",
|
|
206
|
-
value: "",
|
|
207
|
-
type: "separator"
|
|
208
|
-
},
|
|
209
|
-
...localFiles.map((file) => ({
|
|
210
|
-
title: (0, import_path2.basename)(file, ".json"),
|
|
211
|
-
description: `Local file: ${file}`,
|
|
212
|
-
value: (0, import_path2.basename)(file, ".json"),
|
|
213
|
-
type: "local"
|
|
214
|
-
}))
|
|
403
|
+
let filtered = templates2;
|
|
404
|
+
if (options.provider) {
|
|
405
|
+
filtered = templates2.filter(
|
|
406
|
+
(t) => t.metadata.provider.toLowerCase() === options.provider?.toLowerCase()
|
|
215
407
|
);
|
|
216
408
|
}
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
value: "",
|
|
223
|
-
type: "separator"
|
|
224
|
-
},
|
|
225
|
-
...templates.map((template) => ({
|
|
226
|
-
title: template,
|
|
227
|
-
description: `Template: ${TEMPLATES[template]}`,
|
|
228
|
-
value: template,
|
|
229
|
-
type: "template"
|
|
230
|
-
}))
|
|
409
|
+
if (filtered.length === 0) {
|
|
410
|
+
console.log(
|
|
411
|
+
import_chalk.default.yellow(
|
|
412
|
+
`\u{1F4ED} No templates found for provider: ${options.provider}`
|
|
413
|
+
)
|
|
231
414
|
);
|
|
415
|
+
return;
|
|
232
416
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
417
|
+
console.log(import_chalk.default.bold("\n\u{1F4E6} Available Templates\n"));
|
|
418
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
419
|
+
for (const t of filtered) {
|
|
420
|
+
const provider = t.metadata.provider;
|
|
421
|
+
if (!byProvider.has(provider)) {
|
|
422
|
+
byProvider.set(provider, []);
|
|
423
|
+
}
|
|
424
|
+
byProvider.get(provider).push(t);
|
|
425
|
+
}
|
|
426
|
+
for (const [provider, providerTemplates] of byProvider) {
|
|
427
|
+
console.log(import_chalk.default.cyan.bold(` ${provider.toUpperCase()}`));
|
|
428
|
+
for (const t of providerTemplates) {
|
|
429
|
+
const status = t.isDownloaded ? import_chalk.default.green("\u2713 downloaded") : import_chalk.default.gray("\u25CB remote");
|
|
430
|
+
console.log(` ${import_chalk.default.white(t.metadata.id)} ${status}`);
|
|
431
|
+
if (t.metadata.description) {
|
|
432
|
+
console.log(import_chalk.default.gray(` ${t.metadata.description}`));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
console.log();
|
|
436
|
+
}
|
|
437
|
+
console.log(import_chalk.default.gray(` Total: ${filtered.length} templates`));
|
|
438
|
+
console.log(
|
|
439
|
+
import_chalk.default.gray(` Download: better-webhook templates download <id>
|
|
440
|
+
`)
|
|
441
|
+
);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
spinner.fail("Failed to fetch templates");
|
|
444
|
+
console.error(import_chalk.default.red(error.message));
|
|
445
|
+
process.exitCode = 1;
|
|
446
|
+
}
|
|
447
|
+
});
|
|
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(`
|
|
465
|
+
Downloading ${toDownload.length} templates...
|
|
466
|
+
`)
|
|
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
|
+
}
|
|
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;
|
|
486
|
+
}
|
|
244
487
|
return;
|
|
245
488
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const downloadSpinner = (0, import_ora.default)(
|
|
249
|
-
`Downloading template: ${selectedNameOrPath}...`
|
|
250
|
-
).start();
|
|
489
|
+
if (!templateId) {
|
|
490
|
+
const spinner2 = (0, import_ora.default)("Fetching templates...").start();
|
|
251
491
|
try {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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;
|
|
257
500
|
}
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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;
|
|
267
517
|
} catch (error) {
|
|
268
|
-
|
|
518
|
+
spinner2.fail("Failed to fetch templates");
|
|
519
|
+
console.error(import_chalk.default.red(error.message));
|
|
269
520
|
process.exitCode = 1;
|
|
270
521
|
return;
|
|
271
522
|
}
|
|
272
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
|
+
);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
spinner.fail(`Failed to download ${templateId}`);
|
|
535
|
+
console.error(import_chalk.default.red(error.message));
|
|
536
|
+
process.exitCode = 1;
|
|
537
|
+
}
|
|
273
538
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
539
|
+
);
|
|
540
|
+
var localCommand = new import_commander.Command().name("local").description("List downloaded local templates").option("-p, --provider <provider>", "Filter by provider").action((options) => {
|
|
541
|
+
const manager = getTemplateManager();
|
|
542
|
+
let templates2 = manager.listLocalTemplates();
|
|
543
|
+
if (options.provider) {
|
|
544
|
+
templates2 = templates2.filter(
|
|
545
|
+
(t) => t.metadata.provider.toLowerCase() === options.provider?.toLowerCase()
|
|
546
|
+
);
|
|
278
547
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
selectedNameOrPath + (selectedNameOrPath.endsWith(".json") ? "" : ".json")
|
|
548
|
+
if (templates2.length === 0) {
|
|
549
|
+
console.log(import_chalk.default.yellow("\n\u{1F4ED} No local templates found."));
|
|
550
|
+
console.log(
|
|
551
|
+
import_chalk.default.gray(
|
|
552
|
+
" Download templates with: better-webhook templates download\n"
|
|
553
|
+
)
|
|
286
554
|
);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
console.log(import_chalk.default.bold("\n\u{1F4C1} Local Templates\n"));
|
|
558
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
559
|
+
for (const t of templates2) {
|
|
560
|
+
const provider = t.metadata.provider;
|
|
561
|
+
if (!byProvider.has(provider)) {
|
|
562
|
+
byProvider.set(provider, []);
|
|
291
563
|
}
|
|
564
|
+
byProvider.get(provider).push(t);
|
|
292
565
|
}
|
|
293
|
-
|
|
294
|
-
console.
|
|
295
|
-
|
|
296
|
-
|
|
566
|
+
for (const [provider, providerTemplates] of byProvider) {
|
|
567
|
+
console.log(import_chalk.default.cyan.bold(` ${provider.toUpperCase()}`));
|
|
568
|
+
for (const t of providerTemplates) {
|
|
569
|
+
console.log(` ${import_chalk.default.white(t.id)}`);
|
|
570
|
+
console.log(import_chalk.default.gray(` Event: ${t.metadata.event}`));
|
|
571
|
+
console.log(
|
|
572
|
+
import_chalk.default.gray(
|
|
573
|
+
` Downloaded: ${new Date(t.downloadedAt).toLocaleDateString()}`
|
|
574
|
+
)
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
console.log();
|
|
297
578
|
}
|
|
298
|
-
|
|
579
|
+
console.log(import_chalk.default.gray(` Total: ${templates2.length} templates`));
|
|
580
|
+
console.log(import_chalk.default.gray(` Storage: ${manager.getTemplatesDir()}
|
|
581
|
+
`));
|
|
582
|
+
});
|
|
583
|
+
var searchCommand = new import_commander.Command().name("search").argument("<query>", "Search query").description("Search templates by name, provider, or event").action(async (query) => {
|
|
584
|
+
const spinner = (0, import_ora.default)("Searching...").start();
|
|
299
585
|
try {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
586
|
+
const manager = getTemplateManager();
|
|
587
|
+
const results = await manager.searchTemplates(query);
|
|
588
|
+
spinner.stop();
|
|
589
|
+
const totalCount = results.remote.length + results.local.length;
|
|
590
|
+
if (totalCount === 0) {
|
|
591
|
+
console.log(import_chalk.default.yellow(`
|
|
592
|
+
\u{1F4ED} No templates found for: "${query}"
|
|
593
|
+
`));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
console.log(import_chalk.default.bold(`
|
|
597
|
+
\u{1F50D} Search Results for "${query}"
|
|
598
|
+
`));
|
|
599
|
+
if (results.local.length > 0) {
|
|
600
|
+
console.log(import_chalk.default.cyan.bold(" LOCAL TEMPLATES"));
|
|
601
|
+
for (const t of results.local) {
|
|
602
|
+
console.log(
|
|
603
|
+
` ${import_chalk.default.green("\u2713")} ${t.id} (${t.metadata.provider})`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
console.log();
|
|
607
|
+
}
|
|
608
|
+
if (results.remote.length > 0) {
|
|
609
|
+
console.log(import_chalk.default.cyan.bold(" REMOTE TEMPLATES"));
|
|
610
|
+
for (const t of results.remote) {
|
|
611
|
+
const status = t.isDownloaded ? import_chalk.default.green("\u2713") : import_chalk.default.gray("\u25CB");
|
|
612
|
+
console.log(
|
|
613
|
+
` ${status} ${t.metadata.id} (${t.metadata.provider})`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
console.log();
|
|
617
|
+
}
|
|
618
|
+
console.log(import_chalk.default.gray(` Found: ${totalCount} templates
|
|
619
|
+
`));
|
|
620
|
+
} catch (error) {
|
|
621
|
+
spinner.fail("Search failed");
|
|
622
|
+
console.error(import_chalk.default.red(error.message));
|
|
303
623
|
process.exitCode = 1;
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
var cacheCommand = new import_commander.Command().name("cache").description("Manage template cache").option("-c, --clear", "Clear the template cache").action((options) => {
|
|
627
|
+
if (options.clear) {
|
|
628
|
+
const manager = getTemplateManager();
|
|
629
|
+
manager.clearCache();
|
|
630
|
+
console.log(import_chalk.default.green("\u2713 Template cache cleared"));
|
|
631
|
+
} else {
|
|
632
|
+
console.log("Use --clear to clear the template cache");
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
var cleanCommand = new import_commander.Command().name("clean").alias("remove-all").description("Remove all downloaded templates").option("-f, --force", "Skip confirmation prompt").action(async (options) => {
|
|
636
|
+
const manager = getTemplateManager();
|
|
637
|
+
const templates2 = manager.listLocalTemplates();
|
|
638
|
+
if (templates2.length === 0) {
|
|
639
|
+
console.log(import_chalk.default.yellow("\n\u{1F4ED} No local templates to remove.\n"));
|
|
304
640
|
return;
|
|
305
641
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
console.log(result.bodyText);
|
|
642
|
+
console.log(
|
|
643
|
+
import_chalk.default.bold(`
|
|
644
|
+
\u{1F5D1}\uFE0F Found ${templates2.length} downloaded template(s)
|
|
645
|
+
`)
|
|
646
|
+
);
|
|
647
|
+
for (const t of templates2) {
|
|
648
|
+
console.log(import_chalk.default.gray(` ${t.id} (${t.metadata.provider})`));
|
|
649
|
+
}
|
|
650
|
+
console.log();
|
|
651
|
+
if (!options.force) {
|
|
652
|
+
const response = await (0, import_prompts.default)({
|
|
653
|
+
type: "confirm",
|
|
654
|
+
name: "confirm",
|
|
655
|
+
message: `Delete all ${templates2.length} template(s)?`,
|
|
656
|
+
initial: false
|
|
657
|
+
});
|
|
658
|
+
if (!response.confirm) {
|
|
659
|
+
console.log(import_chalk.default.yellow("Cancelled"));
|
|
660
|
+
return;
|
|
326
661
|
}
|
|
327
|
-
} catch (err) {
|
|
328
|
-
executeSpinner.fail("Request failed");
|
|
329
|
-
console.error("Error:", err.message);
|
|
330
|
-
process.exitCode = 1;
|
|
331
662
|
}
|
|
663
|
+
const deleted = manager.deleteAllLocalTemplates();
|
|
664
|
+
console.log(import_chalk.default.green(`
|
|
665
|
+
\u2713 Removed ${deleted} template(s)`));
|
|
666
|
+
console.log(import_chalk.default.gray(` Storage: ${manager.getTemplatesDir()}
|
|
667
|
+
`));
|
|
332
668
|
});
|
|
333
|
-
var
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
669
|
+
var templates = new import_commander.Command().name("templates").alias("t").description("Manage webhook templates").addCommand(listCommand).addCommand(downloadCommand).addCommand(localCommand).addCommand(searchCommand).addCommand(cacheCommand).addCommand(cleanCommand);
|
|
670
|
+
|
|
671
|
+
// src/commands/run.ts
|
|
672
|
+
var import_commander2 = require("commander");
|
|
673
|
+
var import_ora2 = __toESM(require("ora"), 1);
|
|
674
|
+
var import_prompts2 = __toESM(require("prompts"), 1);
|
|
675
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
676
|
+
|
|
677
|
+
// src/core/executor.ts
|
|
678
|
+
var import_undici2 = require("undici");
|
|
679
|
+
|
|
680
|
+
// src/core/signature.ts
|
|
681
|
+
var import_crypto = require("crypto");
|
|
682
|
+
function generateStripeSignature(payload, secret, timestamp) {
|
|
683
|
+
const ts = timestamp || Math.floor(Date.now() / 1e3);
|
|
684
|
+
const signedPayload = `${ts}.${payload}`;
|
|
685
|
+
const signature = (0, import_crypto.createHmac)("sha256", secret).update(signedPayload).digest("hex");
|
|
686
|
+
return {
|
|
687
|
+
header: "Stripe-Signature",
|
|
688
|
+
value: `t=${ts},v1=${signature}`
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function generateGitHubSignature(payload, secret) {
|
|
692
|
+
const signature = (0, import_crypto.createHmac)("sha256", secret).update(payload).digest("hex");
|
|
693
|
+
return {
|
|
694
|
+
header: "X-Hub-Signature-256",
|
|
695
|
+
value: `sha256=${signature}`
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
function generateShopifySignature(payload, secret) {
|
|
699
|
+
const signature = (0, import_crypto.createHmac)("sha256", secret).update(payload).digest("base64");
|
|
700
|
+
return {
|
|
701
|
+
header: "X-Shopify-Hmac-SHA256",
|
|
702
|
+
value: signature
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function generateTwilioSignature(payload, secret, url) {
|
|
706
|
+
const signatureInput = url + payload;
|
|
707
|
+
const signature = (0, import_crypto.createHmac)("sha1", secret).update(signatureInput).digest("base64");
|
|
708
|
+
return {
|
|
709
|
+
header: "X-Twilio-Signature",
|
|
710
|
+
value: signature
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function generateSlackSignature(payload, secret, timestamp) {
|
|
714
|
+
const ts = timestamp || Math.floor(Date.now() / 1e3);
|
|
715
|
+
const signatureBaseString = `v0:${ts}:${payload}`;
|
|
716
|
+
const signature = (0, import_crypto.createHmac)("sha256", secret).update(signatureBaseString).digest("hex");
|
|
717
|
+
return {
|
|
718
|
+
header: "X-Slack-Signature",
|
|
719
|
+
value: `v0=${signature}`
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function generateLinearSignature(payload, secret) {
|
|
723
|
+
const signature = (0, import_crypto.createHmac)("sha256", secret).update(payload).digest("hex");
|
|
724
|
+
return {
|
|
725
|
+
header: "Linear-Signature",
|
|
726
|
+
value: signature
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function generateClerkSignature(payload, secret, timestamp, webhookId) {
|
|
730
|
+
const ts = timestamp || Math.floor(Date.now() / 1e3);
|
|
731
|
+
const msgId = webhookId || `msg_${Date.now()}`;
|
|
732
|
+
const signedPayload = `${msgId}.${ts}.${payload}`;
|
|
733
|
+
const signature = (0, import_crypto.createHmac)("sha256", secret).update(signedPayload).digest("base64");
|
|
734
|
+
return {
|
|
735
|
+
header: "Svix-Signature",
|
|
736
|
+
value: `v1,${signature}`
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function generateSendGridSignature(payload, secret, timestamp) {
|
|
740
|
+
const ts = timestamp || Math.floor(Date.now() / 1e3);
|
|
741
|
+
const signedPayload = `${ts}${payload}`;
|
|
742
|
+
const signature = (0, import_crypto.createHmac)("sha256", secret).update(signedPayload).digest("base64");
|
|
743
|
+
return {
|
|
744
|
+
header: "X-Twilio-Email-Event-Webhook-Signature",
|
|
745
|
+
value: signature
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function generateSignature(provider, payload, secret, options) {
|
|
749
|
+
const timestamp = options?.timestamp;
|
|
750
|
+
switch (provider) {
|
|
751
|
+
case "stripe":
|
|
752
|
+
return generateStripeSignature(payload, secret, timestamp);
|
|
753
|
+
case "github":
|
|
754
|
+
return generateGitHubSignature(payload, secret);
|
|
755
|
+
case "shopify":
|
|
756
|
+
return generateShopifySignature(payload, secret);
|
|
757
|
+
case "twilio":
|
|
758
|
+
if (!options?.url) {
|
|
759
|
+
throw new Error("Twilio signature requires URL");
|
|
760
|
+
}
|
|
761
|
+
return generateTwilioSignature(payload, secret, options.url);
|
|
762
|
+
case "slack":
|
|
763
|
+
return generateSlackSignature(payload, secret, timestamp);
|
|
764
|
+
case "linear":
|
|
765
|
+
return generateLinearSignature(payload, secret);
|
|
766
|
+
case "clerk":
|
|
767
|
+
return generateClerkSignature(
|
|
768
|
+
payload,
|
|
769
|
+
secret,
|
|
770
|
+
timestamp,
|
|
771
|
+
options?.webhookId
|
|
772
|
+
);
|
|
773
|
+
case "sendgrid":
|
|
774
|
+
return generateSendGridSignature(payload, secret, timestamp);
|
|
775
|
+
case "discord":
|
|
776
|
+
case "custom":
|
|
777
|
+
default:
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function getProviderHeaders(provider, options) {
|
|
782
|
+
const headers = [];
|
|
783
|
+
const timestamp = options?.timestamp || Math.floor(Date.now() / 1e3);
|
|
784
|
+
switch (provider) {
|
|
785
|
+
case "stripe":
|
|
786
|
+
headers.push(
|
|
787
|
+
{ key: "Content-Type", value: "application/json" },
|
|
788
|
+
{
|
|
789
|
+
key: "User-Agent",
|
|
790
|
+
value: "Stripe/1.0 (+https://stripe.com/docs/webhooks)"
|
|
791
|
+
}
|
|
792
|
+
);
|
|
793
|
+
break;
|
|
794
|
+
case "github":
|
|
795
|
+
headers.push(
|
|
796
|
+
{ key: "Content-Type", value: "application/json" },
|
|
797
|
+
{ key: "User-Agent", value: "GitHub-Hookshot/better-webhook" },
|
|
798
|
+
{ key: "X-GitHub-Event", value: options?.event || "push" },
|
|
799
|
+
{
|
|
800
|
+
key: "X-GitHub-Delivery",
|
|
801
|
+
value: options?.webhookId || generateDeliveryId()
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
break;
|
|
805
|
+
case "shopify":
|
|
806
|
+
headers.push(
|
|
807
|
+
{ key: "Content-Type", value: "application/json" },
|
|
808
|
+
{ key: "X-Shopify-Topic", value: options?.event || "orders/create" },
|
|
809
|
+
{ key: "X-Shopify-Shop-Domain", value: "example.myshopify.com" },
|
|
810
|
+
{ key: "X-Shopify-API-Version", value: "2024-01" }
|
|
811
|
+
);
|
|
812
|
+
break;
|
|
813
|
+
case "slack":
|
|
814
|
+
headers.push(
|
|
815
|
+
{ key: "Content-Type", value: "application/json" },
|
|
816
|
+
{ key: "X-Slack-Request-Timestamp", value: String(timestamp) }
|
|
817
|
+
);
|
|
818
|
+
break;
|
|
819
|
+
case "clerk":
|
|
820
|
+
headers.push(
|
|
821
|
+
{ key: "Content-Type", value: "application/json" },
|
|
822
|
+
{ key: "Svix-Id", value: options?.webhookId || `msg_${Date.now()}` },
|
|
823
|
+
{ key: "Svix-Timestamp", value: String(timestamp) }
|
|
824
|
+
);
|
|
825
|
+
break;
|
|
826
|
+
case "sendgrid":
|
|
827
|
+
headers.push(
|
|
828
|
+
{ key: "Content-Type", value: "application/json" },
|
|
829
|
+
{
|
|
830
|
+
key: "X-Twilio-Email-Event-Webhook-Timestamp",
|
|
831
|
+
value: String(timestamp)
|
|
832
|
+
}
|
|
833
|
+
);
|
|
834
|
+
break;
|
|
835
|
+
case "twilio":
|
|
836
|
+
headers.push({
|
|
837
|
+
key: "Content-Type",
|
|
838
|
+
value: "application/x-www-form-urlencoded"
|
|
839
|
+
});
|
|
840
|
+
break;
|
|
841
|
+
case "linear":
|
|
842
|
+
headers.push(
|
|
843
|
+
{ key: "Content-Type", value: "application/json" },
|
|
844
|
+
{
|
|
845
|
+
key: "Linear-Delivery",
|
|
846
|
+
value: options?.webhookId || generateDeliveryId()
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
break;
|
|
850
|
+
case "discord":
|
|
851
|
+
headers.push(
|
|
852
|
+
{ key: "Content-Type", value: "application/json" },
|
|
853
|
+
{ key: "User-Agent", value: "Discord-Webhook/1.0" }
|
|
854
|
+
);
|
|
855
|
+
break;
|
|
856
|
+
default:
|
|
857
|
+
headers.push({ key: "Content-Type", value: "application/json" });
|
|
858
|
+
}
|
|
859
|
+
return headers;
|
|
860
|
+
}
|
|
861
|
+
function generateDeliveryId() {
|
|
862
|
+
const chars = "0123456789abcdef";
|
|
863
|
+
let id = "";
|
|
864
|
+
for (let i = 0; i < 36; i++) {
|
|
865
|
+
if (i === 8 || i === 13 || i === 18 || i === 23) {
|
|
866
|
+
id += "-";
|
|
867
|
+
} else {
|
|
868
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
341
869
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
870
|
+
}
|
|
871
|
+
return id;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/core/executor.ts
|
|
875
|
+
async function executeWebhook(options) {
|
|
876
|
+
const startTime = Date.now();
|
|
877
|
+
let bodyStr;
|
|
878
|
+
if (options.body !== void 0) {
|
|
879
|
+
bodyStr = typeof options.body === "string" ? options.body : JSON.stringify(options.body);
|
|
880
|
+
}
|
|
881
|
+
const headers = {};
|
|
882
|
+
if (options.provider) {
|
|
883
|
+
const providerHeaders = getProviderHeaders(options.provider);
|
|
884
|
+
for (const h of providerHeaders) {
|
|
885
|
+
headers[h.key] = h.value;
|
|
351
886
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
887
|
+
}
|
|
888
|
+
if (options.headers) {
|
|
889
|
+
for (const h of options.headers) {
|
|
890
|
+
headers[h.key] = h.value;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (options.secret && options.provider && bodyStr) {
|
|
894
|
+
const sig = generateSignature(options.provider, bodyStr, options.secret, {
|
|
895
|
+
url: options.url
|
|
896
|
+
});
|
|
897
|
+
if (sig) {
|
|
898
|
+
headers[sig.header] = sig.value;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (!headers["Content-Type"] && !headers["content-type"]) {
|
|
902
|
+
headers["Content-Type"] = "application/json";
|
|
903
|
+
}
|
|
904
|
+
try {
|
|
905
|
+
const response = await (0, import_undici2.request)(options.url, {
|
|
906
|
+
method: options.method || "POST",
|
|
907
|
+
headers,
|
|
908
|
+
body: bodyStr,
|
|
909
|
+
headersTimeout: options.timeout || 3e4,
|
|
910
|
+
bodyTimeout: options.timeout || 3e4
|
|
911
|
+
});
|
|
912
|
+
const bodyText = await response.body.text();
|
|
913
|
+
const duration = Date.now() - startTime;
|
|
914
|
+
const responseHeaders = {};
|
|
915
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
916
|
+
if (value !== void 0) {
|
|
917
|
+
responseHeaders[key] = value;
|
|
359
918
|
}
|
|
360
|
-
|
|
919
|
+
}
|
|
920
|
+
let json;
|
|
921
|
+
try {
|
|
922
|
+
json = JSON.parse(bodyText);
|
|
923
|
+
} catch {
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
status: response.statusCode,
|
|
927
|
+
statusText: getStatusText(response.statusCode),
|
|
928
|
+
headers: responseHeaders,
|
|
929
|
+
body: json ?? bodyText,
|
|
930
|
+
bodyText,
|
|
931
|
+
json,
|
|
932
|
+
duration
|
|
933
|
+
};
|
|
934
|
+
} catch (error) {
|
|
935
|
+
const duration = Date.now() - startTime;
|
|
936
|
+
throw new ExecutionError(error.message, duration);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
async function executeTemplate(template, options = {}) {
|
|
940
|
+
const targetUrl = options.url || template.url;
|
|
941
|
+
if (!targetUrl) {
|
|
942
|
+
throw new Error(
|
|
943
|
+
"No target URL specified. Use --url or set url in template."
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
const mergedHeaders = [...template.headers || []];
|
|
947
|
+
if (options.headers) {
|
|
948
|
+
for (const h of options.headers) {
|
|
949
|
+
const existingIdx = mergedHeaders.findIndex(
|
|
950
|
+
(mh) => mh.key.toLowerCase() === h.key.toLowerCase()
|
|
951
|
+
);
|
|
952
|
+
if (existingIdx >= 0) {
|
|
953
|
+
mergedHeaders[existingIdx] = h;
|
|
954
|
+
} else {
|
|
955
|
+
mergedHeaders.push(h);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return executeWebhook({
|
|
960
|
+
url: targetUrl,
|
|
961
|
+
method: template.method,
|
|
962
|
+
headers: mergedHeaders,
|
|
963
|
+
body: template.body,
|
|
964
|
+
secret: options.secret,
|
|
965
|
+
provider: template.provider
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
var ExecutionError = class extends Error {
|
|
969
|
+
duration;
|
|
970
|
+
constructor(message, duration) {
|
|
971
|
+
super(message);
|
|
972
|
+
this.name = "ExecutionError";
|
|
973
|
+
this.duration = duration;
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
function getStatusText(code) {
|
|
977
|
+
const statusTexts = {
|
|
978
|
+
200: "OK",
|
|
979
|
+
201: "Created",
|
|
980
|
+
202: "Accepted",
|
|
981
|
+
204: "No Content",
|
|
982
|
+
301: "Moved Permanently",
|
|
983
|
+
302: "Found",
|
|
984
|
+
304: "Not Modified",
|
|
985
|
+
400: "Bad Request",
|
|
986
|
+
401: "Unauthorized",
|
|
987
|
+
403: "Forbidden",
|
|
988
|
+
404: "Not Found",
|
|
989
|
+
405: "Method Not Allowed",
|
|
990
|
+
408: "Request Timeout",
|
|
991
|
+
409: "Conflict",
|
|
992
|
+
422: "Unprocessable Entity",
|
|
993
|
+
429: "Too Many Requests",
|
|
994
|
+
500: "Internal Server Error",
|
|
995
|
+
502: "Bad Gateway",
|
|
996
|
+
503: "Service Unavailable",
|
|
997
|
+
504: "Gateway Timeout"
|
|
998
|
+
};
|
|
999
|
+
return statusTexts[code] || "Unknown";
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/commands/run.ts
|
|
1003
|
+
function getSecretEnvVarName(provider) {
|
|
1004
|
+
const envVarMap = {
|
|
1005
|
+
github: "GITHUB_WEBHOOK_SECRET",
|
|
1006
|
+
stripe: "STRIPE_WEBHOOK_SECRET",
|
|
1007
|
+
shopify: "SHOPIFY_WEBHOOK_SECRET",
|
|
1008
|
+
twilio: "TWILIO_WEBHOOK_SECRET",
|
|
1009
|
+
ragie: "RAGIE_WEBHOOK_SECRET",
|
|
1010
|
+
slack: "SLACK_WEBHOOK_SECRET",
|
|
1011
|
+
linear: "LINEAR_WEBHOOK_SECRET",
|
|
1012
|
+
clerk: "CLERK_WEBHOOK_SECRET",
|
|
1013
|
+
sendgrid: "SENDGRID_WEBHOOK_SECRET",
|
|
1014
|
+
discord: "DISCORD_WEBHOOK_SECRET",
|
|
1015
|
+
custom: "WEBHOOK_SECRET"
|
|
1016
|
+
};
|
|
1017
|
+
return envVarMap[provider] || "WEBHOOK_SECRET";
|
|
1018
|
+
}
|
|
1019
|
+
var run = new import_commander2.Command().name("run").argument("[templateId]", "Template ID to run").description("Run a webhook template against a target URL").requiredOption("-u, --url <url>", "Target URL to send the webhook to").option("-s, --secret <secret>", "Secret for signature generation").option(
|
|
1020
|
+
"-H, --header <header>",
|
|
1021
|
+
"Add custom header (format: key:value)",
|
|
1022
|
+
(value, previous) => {
|
|
1023
|
+
const [key, ...valueParts] = value.split(":");
|
|
1024
|
+
const headerValue = valueParts.join(":");
|
|
1025
|
+
if (!key || !headerValue) {
|
|
1026
|
+
throw new Error("Header format should be key:value");
|
|
1027
|
+
}
|
|
1028
|
+
return (previous || []).concat([
|
|
1029
|
+
{ key: key.trim(), value: headerValue.trim() }
|
|
1030
|
+
]);
|
|
1031
|
+
},
|
|
1032
|
+
[]
|
|
1033
|
+
).option("-v, --verbose", "Show detailed request/response information").action(
|
|
1034
|
+
async (templateId, options) => {
|
|
1035
|
+
const manager = getTemplateManager();
|
|
1036
|
+
if (!templateId) {
|
|
1037
|
+
const spinner2 = (0, import_ora2.default)("Loading templates...").start();
|
|
361
1038
|
try {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
1039
|
+
const local = manager.listLocalTemplates();
|
|
1040
|
+
const remote = await manager.listRemoteTemplates();
|
|
1041
|
+
spinner2.stop();
|
|
1042
|
+
if (local.length === 0 && remote.length === 0) {
|
|
1043
|
+
console.log(import_chalk2.default.yellow("\n\u{1F4ED} No templates available."));
|
|
1044
|
+
console.log(
|
|
1045
|
+
import_chalk2.default.gray(
|
|
1046
|
+
" Download templates with: better-webhook templates download\n"
|
|
1047
|
+
)
|
|
366
1048
|
);
|
|
367
|
-
|
|
1049
|
+
return;
|
|
368
1050
|
}
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
1051
|
+
const choices = [];
|
|
1052
|
+
if (local.length > 0) {
|
|
1053
|
+
for (const t of local) {
|
|
1054
|
+
choices.push({
|
|
1055
|
+
title: `${t.id} ${import_chalk2.default.green("(local)")}`,
|
|
1056
|
+
description: `${t.metadata.provider} - ${t.metadata.event}`,
|
|
1057
|
+
value: t.id
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
378
1060
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
1061
|
+
const remoteOnly = remote.filter((t) => !t.isDownloaded);
|
|
1062
|
+
for (const t of remoteOnly) {
|
|
1063
|
+
choices.push({
|
|
1064
|
+
title: `${t.metadata.id} ${import_chalk2.default.gray("(remote)")}`,
|
|
1065
|
+
description: `${t.metadata.provider} - ${t.metadata.event}`,
|
|
1066
|
+
value: `remote:${t.metadata.id}`
|
|
1067
|
+
});
|
|
384
1068
|
}
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
1069
|
+
const response = await (0, import_prompts2.default)({
|
|
1070
|
+
type: "select",
|
|
1071
|
+
name: "templateId",
|
|
1072
|
+
message: "Select a template to run:",
|
|
1073
|
+
choices
|
|
1074
|
+
});
|
|
1075
|
+
if (!response.templateId) {
|
|
1076
|
+
console.log(import_chalk2.default.yellow("Cancelled"));
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
templateId = response.templateId;
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
spinner2.fail("Failed to load templates");
|
|
1082
|
+
console.error(import_chalk2.default.red(error.message));
|
|
1083
|
+
process.exitCode = 1;
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (templateId?.startsWith("remote:")) {
|
|
1088
|
+
const remoteId = templateId.replace("remote:", "");
|
|
1089
|
+
const downloadSpinner = (0, import_ora2.default)(`Downloading ${remoteId}...`).start();
|
|
1090
|
+
try {
|
|
1091
|
+
await manager.downloadTemplate(remoteId);
|
|
1092
|
+
downloadSpinner.succeed(`Downloaded ${remoteId}`);
|
|
1093
|
+
templateId = remoteId;
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
downloadSpinner.fail(`Failed to download: ${error.message}`);
|
|
1096
|
+
process.exitCode = 1;
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
const localTemplate = manager.getLocalTemplate(templateId);
|
|
1101
|
+
if (!localTemplate) {
|
|
1102
|
+
console.log(import_chalk2.default.red(`
|
|
1103
|
+
\u274C Template not found: ${templateId}`));
|
|
1104
|
+
console.log(
|
|
1105
|
+
import_chalk2.default.gray(
|
|
1106
|
+
" Download it with: better-webhook templates download " + templateId + "\n"
|
|
1107
|
+
)
|
|
1108
|
+
);
|
|
1109
|
+
process.exitCode = 1;
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const targetUrl = options.url;
|
|
1113
|
+
let secret = options?.secret;
|
|
1114
|
+
if (!secret && localTemplate.metadata.provider) {
|
|
1115
|
+
const envVarName = getSecretEnvVarName(localTemplate.metadata.provider);
|
|
1116
|
+
secret = process.env[envVarName];
|
|
1117
|
+
}
|
|
1118
|
+
console.log(import_chalk2.default.bold("\n\u{1F680} Executing Webhook\n"));
|
|
1119
|
+
console.log(import_chalk2.default.gray(` Template: ${templateId}`));
|
|
1120
|
+
console.log(
|
|
1121
|
+
import_chalk2.default.gray(` Provider: ${localTemplate.metadata.provider}`)
|
|
1122
|
+
);
|
|
1123
|
+
console.log(import_chalk2.default.gray(` Event: ${localTemplate.metadata.event}`));
|
|
1124
|
+
console.log(import_chalk2.default.gray(` Target: ${targetUrl}`));
|
|
1125
|
+
if (secret) {
|
|
1126
|
+
console.log(import_chalk2.default.gray(` Signature: Will be generated`));
|
|
1127
|
+
} else {
|
|
1128
|
+
console.log(
|
|
1129
|
+
import_chalk2.default.yellow(
|
|
1130
|
+
` \u26A0\uFE0F No secret provided - signature will not be generated`
|
|
1131
|
+
)
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
console.log();
|
|
1135
|
+
const spinner = (0, import_ora2.default)("Sending webhook...").start();
|
|
1136
|
+
try {
|
|
1137
|
+
const result = await executeTemplate(localTemplate.template, {
|
|
1138
|
+
url: targetUrl,
|
|
1139
|
+
secret,
|
|
1140
|
+
headers: options?.header
|
|
1141
|
+
});
|
|
1142
|
+
spinner.stop();
|
|
1143
|
+
const statusColor = result.status >= 200 && result.status < 300 ? import_chalk2.default.green : result.status >= 400 ? import_chalk2.default.red : import_chalk2.default.yellow;
|
|
1144
|
+
console.log(import_chalk2.default.bold("\u{1F4E5} Response\n"));
|
|
1145
|
+
console.log(
|
|
1146
|
+
` Status: ${statusColor(`${result.status} ${result.statusText}`)}`
|
|
1147
|
+
);
|
|
1148
|
+
console.log(` Duration: ${import_chalk2.default.cyan(`${result.duration}ms`)}`);
|
|
1149
|
+
if (options?.verbose) {
|
|
1150
|
+
console.log(import_chalk2.default.bold("\n Headers:"));
|
|
1151
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
1152
|
+
const headerValue = Array.isArray(value) ? value.join(", ") : value;
|
|
1153
|
+
console.log(import_chalk2.default.gray(` ${key}: ${headerValue}`));
|
|
392
1154
|
}
|
|
393
|
-
(0, import_fs3.writeFileSync)(destPath, JSON.stringify(json, null, 2));
|
|
394
|
-
console.log(`Downloaded ${templateName} -> .webhooks/${fileName}`);
|
|
395
|
-
} catch (e) {
|
|
396
|
-
console.error(`Error downloading ${templateName}: ${e.message}`);
|
|
397
1155
|
}
|
|
1156
|
+
if (result.json !== void 0) {
|
|
1157
|
+
console.log(import_chalk2.default.bold("\n Body:"));
|
|
1158
|
+
console.log(
|
|
1159
|
+
import_chalk2.default.gray(
|
|
1160
|
+
JSON.stringify(result.json, null, 2).split("\n").map((l) => ` ${l}`).join("\n")
|
|
1161
|
+
)
|
|
1162
|
+
);
|
|
1163
|
+
} else if (result.bodyText) {
|
|
1164
|
+
console.log(import_chalk2.default.bold("\n Body:"));
|
|
1165
|
+
const preview = result.bodyText.length > 500 ? result.bodyText.slice(0, 500) + "..." : result.bodyText;
|
|
1166
|
+
console.log(import_chalk2.default.gray(` ${preview}`));
|
|
1167
|
+
}
|
|
1168
|
+
console.log();
|
|
1169
|
+
if (result.status >= 200 && result.status < 300) {
|
|
1170
|
+
console.log(import_chalk2.default.green("\u2713 Webhook delivered successfully\n"));
|
|
1171
|
+
} else {
|
|
1172
|
+
console.log(
|
|
1173
|
+
import_chalk2.default.yellow(`\u26A0 Webhook delivered with status ${result.status}
|
|
1174
|
+
`)
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
spinner.fail("Request failed");
|
|
1179
|
+
console.error(import_chalk2.default.red(`
|
|
1180
|
+
\u274C ${error.message}
|
|
1181
|
+
`));
|
|
1182
|
+
process.exitCode = 1;
|
|
398
1183
|
}
|
|
399
1184
|
}
|
|
400
1185
|
);
|
|
401
|
-
var webhooks = new import_commander.Command().name("webhooks").description("Manage and execute webhook definitions").addCommand(listCommand).addCommand(runCommand).addCommand(downloadCommand);
|
|
402
1186
|
|
|
403
1187
|
// src/commands/capture.ts
|
|
404
|
-
var
|
|
405
|
-
var
|
|
406
|
-
var import_path5 = require("path");
|
|
1188
|
+
var import_commander3 = require("commander");
|
|
1189
|
+
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
407
1190
|
|
|
408
|
-
// src/capture.ts
|
|
409
|
-
var
|
|
410
|
-
var
|
|
411
|
-
var
|
|
412
|
-
var
|
|
413
|
-
var
|
|
1191
|
+
// src/core/capture-server.ts
|
|
1192
|
+
var import_http = require("http");
|
|
1193
|
+
var import_ws = require("ws");
|
|
1194
|
+
var import_fs2 = require("fs");
|
|
1195
|
+
var import_path2 = require("path");
|
|
1196
|
+
var import_crypto2 = require("crypto");
|
|
1197
|
+
var import_os2 = require("os");
|
|
1198
|
+
var CaptureServer = class {
|
|
414
1199
|
server = null;
|
|
1200
|
+
wss = null;
|
|
415
1201
|
capturesDir;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
1202
|
+
clients = /* @__PURE__ */ new Set();
|
|
1203
|
+
captureCount = 0;
|
|
1204
|
+
enableWebSocket;
|
|
1205
|
+
onCapture;
|
|
1206
|
+
constructor(options) {
|
|
1207
|
+
const capturesDir = typeof options === "string" ? options : options?.capturesDir;
|
|
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;
|
|
1211
|
+
if (!(0, import_fs2.existsSync)(this.capturesDir)) {
|
|
1212
|
+
(0, import_fs2.mkdirSync)(this.capturesDir, { recursive: true });
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Get the captures directory path
|
|
1217
|
+
*/
|
|
1218
|
+
getCapturesDir() {
|
|
1219
|
+
return this.capturesDir;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Start the capture server
|
|
1223
|
+
*/
|
|
1224
|
+
async start(port = 3001, host = "0.0.0.0") {
|
|
1225
|
+
return new Promise((resolve, reject) => {
|
|
1226
|
+
this.server = (0, import_http.createServer)((req, res) => this.handleRequest(req, res));
|
|
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
|
|
437
1245
|
}
|
|
438
|
-
}
|
|
1246
|
+
});
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
this.server.on("error", (err) => {
|
|
1250
|
+
if (err.code === "EADDRINUSE") {
|
|
1251
|
+
reject(new Error(`Port ${port} is already in use`));
|
|
1252
|
+
} else {
|
|
1253
|
+
reject(err);
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
this.server.listen(port, host, () => {
|
|
1257
|
+
const address = this.server?.address();
|
|
1258
|
+
const actualPort = typeof address === "object" ? address?.port || port : port;
|
|
1259
|
+
console.log(`
|
|
1260
|
+
\u{1F3A3} Webhook Capture Server`);
|
|
1261
|
+
console.log(
|
|
1262
|
+
` Listening on http://${host === "0.0.0.0" ? "localhost" : host}:${actualPort}`
|
|
439
1263
|
);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (attempt >= maxAttempts) {
|
|
450
|
-
reject(
|
|
451
|
-
new Error(
|
|
452
|
-
`All ${maxAttempts} port attempts starting at ${startPort} are in use.`
|
|
453
|
-
)
|
|
454
|
-
);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
const nextPort = startPort + attempt;
|
|
458
|
-
tryListen(nextPort);
|
|
459
|
-
} else {
|
|
460
|
-
reject(err);
|
|
461
|
-
}
|
|
462
|
-
};
|
|
463
|
-
const onListening = () => {
|
|
464
|
-
this.server?.off("error", onError);
|
|
465
|
-
this.server?.off("listening", onListening);
|
|
466
|
-
const address = this.server?.address();
|
|
467
|
-
const actualPort = address?.port ?? portToTry;
|
|
468
|
-
console.log(
|
|
469
|
-
`\u{1F3A3} Webhook capture server running on http://localhost:${actualPort}`
|
|
470
|
-
);
|
|
471
|
-
console.log(
|
|
472
|
-
`\u{1F4C1} Captured webhooks will be saved to: ${this.capturesDir}`
|
|
473
|
-
);
|
|
474
|
-
console.log(
|
|
475
|
-
"\u{1F4A1} Send webhooks to any path on this server to capture them"
|
|
476
|
-
);
|
|
477
|
-
console.log("\u23F9\uFE0F Press Ctrl+C to stop the server");
|
|
478
|
-
resolve4(actualPort);
|
|
479
|
-
};
|
|
480
|
-
this.server.on("error", onError);
|
|
481
|
-
this.server.on("listening", onListening);
|
|
482
|
-
this.server.listen(portToTry);
|
|
483
|
-
};
|
|
484
|
-
tryListen(startPort === 0 ? 0 : startPort);
|
|
1264
|
+
console.log(` \u{1F4C1} Captures saved to: ${this.capturesDir}`);
|
|
1265
|
+
console.log(` \u{1F4A1} Send webhooks to any path to capture them`);
|
|
1266
|
+
if (this.enableWebSocket) {
|
|
1267
|
+
console.log(` \u{1F310} WebSocket available for real-time updates`);
|
|
1268
|
+
}
|
|
1269
|
+
console.log(` \u23F9\uFE0F Press Ctrl+C to stop
|
|
1270
|
+
`);
|
|
1271
|
+
resolve(actualPort);
|
|
1272
|
+
});
|
|
485
1273
|
});
|
|
486
1274
|
}
|
|
487
|
-
|
|
488
|
-
|
|
1275
|
+
/**
|
|
1276
|
+
* Stop the capture server
|
|
1277
|
+
*/
|
|
1278
|
+
async stop() {
|
|
1279
|
+
return new Promise((resolve) => {
|
|
1280
|
+
for (const client of this.clients) {
|
|
1281
|
+
client.close();
|
|
1282
|
+
}
|
|
1283
|
+
this.clients.clear();
|
|
1284
|
+
if (this.wss) {
|
|
1285
|
+
this.wss.close();
|
|
1286
|
+
this.wss = null;
|
|
1287
|
+
}
|
|
489
1288
|
if (this.server) {
|
|
490
1289
|
this.server.close(() => {
|
|
491
|
-
console.log("\u{
|
|
492
|
-
|
|
1290
|
+
console.log("\n\u{1F6D1} Capture server stopped");
|
|
1291
|
+
resolve();
|
|
493
1292
|
});
|
|
494
1293
|
} else {
|
|
495
|
-
|
|
1294
|
+
resolve();
|
|
496
1295
|
}
|
|
497
1296
|
});
|
|
498
1297
|
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Handle incoming HTTP requests
|
|
1300
|
+
*/
|
|
499
1301
|
async handleRequest(req, res) {
|
|
1302
|
+
if (req.headers.upgrade?.toLowerCase() === "websocket") {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
500
1305
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
501
|
-
const id =
|
|
1306
|
+
const id = (0, import_crypto2.randomUUID)();
|
|
502
1307
|
const url = req.url || "/";
|
|
503
1308
|
const urlParts = new URL(url, `http://${req.headers.host || "localhost"}`);
|
|
504
1309
|
const query = {};
|
|
@@ -514,299 +1319,659 @@ var WebhookCaptureServer = class {
|
|
|
514
1319
|
}
|
|
515
1320
|
}
|
|
516
1321
|
const chunks = [];
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
});
|
|
1322
|
+
for await (const chunk of req) {
|
|
1323
|
+
chunks.push(chunk);
|
|
1324
|
+
}
|
|
521
1325
|
const rawBody = Buffer.concat(chunks).toString("utf8");
|
|
522
|
-
let
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1326
|
+
let body = null;
|
|
1327
|
+
const contentType = req.headers["content-type"] || "";
|
|
1328
|
+
if (rawBody) {
|
|
1329
|
+
if (contentType.includes("application/json")) {
|
|
1330
|
+
try {
|
|
1331
|
+
body = JSON.parse(rawBody);
|
|
1332
|
+
} catch {
|
|
1333
|
+
body = rawBody;
|
|
1334
|
+
}
|
|
1335
|
+
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
1336
|
+
body = Object.fromEntries(new URLSearchParams(rawBody));
|
|
1337
|
+
} else {
|
|
1338
|
+
body = rawBody;
|
|
1339
|
+
}
|
|
527
1340
|
}
|
|
1341
|
+
const provider = this.detectProvider(req.headers);
|
|
528
1342
|
const captured = {
|
|
529
1343
|
id,
|
|
530
1344
|
timestamp,
|
|
531
1345
|
method: req.method || "GET",
|
|
532
|
-
url
|
|
1346
|
+
url,
|
|
1347
|
+
path: urlParts.pathname,
|
|
533
1348
|
headers: req.headers,
|
|
534
|
-
body
|
|
1349
|
+
body,
|
|
535
1350
|
rawBody,
|
|
536
|
-
query
|
|
1351
|
+
query,
|
|
1352
|
+
provider,
|
|
1353
|
+
contentType: contentType || void 0,
|
|
1354
|
+
contentLength: rawBody.length
|
|
537
1355
|
};
|
|
538
|
-
const
|
|
539
|
-
const
|
|
1356
|
+
const date = new Date(timestamp);
|
|
1357
|
+
const dateStr = date.toISOString().split("T")[0];
|
|
1358
|
+
const timeStr = date.toISOString().split("T")[1]?.replace(/[:.]/g, "-").slice(0, 8);
|
|
1359
|
+
const filename = `${dateStr}_${timeStr}_${id.slice(0, 8)}.json`;
|
|
1360
|
+
const filepath = (0, import_path2.join)(this.capturesDir, filename);
|
|
540
1361
|
try {
|
|
541
|
-
(0,
|
|
1362
|
+
(0, import_fs2.writeFileSync)(filepath, JSON.stringify(captured, null, 2));
|
|
1363
|
+
this.captureCount++;
|
|
1364
|
+
const providerStr = provider ? ` [${provider}]` : "";
|
|
542
1365
|
console.log(
|
|
543
|
-
`\u{1F4E6}
|
|
1366
|
+
`\u{1F4E6} ${req.method} ${urlParts.pathname}${providerStr} -> ${filename}`
|
|
544
1367
|
);
|
|
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
|
+
}
|
|
545
1378
|
} catch (error) {
|
|
546
|
-
console.error(`\u274C Failed to save capture
|
|
1379
|
+
console.error(`\u274C Failed to save capture:`, error);
|
|
547
1380
|
}
|
|
548
1381
|
res.statusCode = 200;
|
|
549
1382
|
res.setHeader("Content-Type", "application/json");
|
|
1383
|
+
res.setHeader("X-Capture-Id", id);
|
|
550
1384
|
res.end(
|
|
551
|
-
JSON.stringify(
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
null,
|
|
559
|
-
2
|
|
560
|
-
)
|
|
1385
|
+
JSON.stringify({
|
|
1386
|
+
success: true,
|
|
1387
|
+
message: "Webhook captured successfully",
|
|
1388
|
+
id,
|
|
1389
|
+
timestamp,
|
|
1390
|
+
file: filename
|
|
1391
|
+
})
|
|
561
1392
|
);
|
|
562
1393
|
}
|
|
563
|
-
|
|
1394
|
+
/**
|
|
1395
|
+
* Detect webhook provider from headers
|
|
1396
|
+
*/
|
|
1397
|
+
detectProvider(headers) {
|
|
1398
|
+
if (headers["stripe-signature"]) {
|
|
1399
|
+
return "stripe";
|
|
1400
|
+
}
|
|
1401
|
+
if (headers["x-github-event"] || headers["x-hub-signature-256"]) {
|
|
1402
|
+
return "github";
|
|
1403
|
+
}
|
|
1404
|
+
if (headers["x-ragie-delivery"]) {
|
|
1405
|
+
return "ragie";
|
|
1406
|
+
}
|
|
1407
|
+
if (headers["x-shopify-hmac-sha256"] || headers["x-shopify-topic"]) {
|
|
1408
|
+
return "shopify";
|
|
1409
|
+
}
|
|
1410
|
+
if (headers["x-twilio-signature"]) {
|
|
1411
|
+
return "twilio";
|
|
1412
|
+
}
|
|
1413
|
+
if (headers["x-twilio-email-event-webhook-signature"]) {
|
|
1414
|
+
return "sendgrid";
|
|
1415
|
+
}
|
|
1416
|
+
if (headers["x-slack-signature"]) {
|
|
1417
|
+
return "slack";
|
|
1418
|
+
}
|
|
1419
|
+
if (headers["x-signature-ed25519"]) {
|
|
1420
|
+
return "discord";
|
|
1421
|
+
}
|
|
1422
|
+
if (headers["linear-signature"]) {
|
|
1423
|
+
return "linear";
|
|
1424
|
+
}
|
|
1425
|
+
if (headers["svix-signature"]) {
|
|
1426
|
+
return "clerk";
|
|
1427
|
+
}
|
|
1428
|
+
return void 0;
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Broadcast message to all connected WebSocket clients
|
|
1432
|
+
*/
|
|
1433
|
+
broadcast(message) {
|
|
1434
|
+
const data = JSON.stringify(message);
|
|
1435
|
+
for (const client of this.clients) {
|
|
1436
|
+
if (client.readyState === 1) {
|
|
1437
|
+
client.send(data);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Send message to a specific client
|
|
1443
|
+
*/
|
|
1444
|
+
sendToClient(client, message) {
|
|
1445
|
+
if (client.readyState === 1) {
|
|
1446
|
+
client.send(JSON.stringify(message));
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* List all captured webhooks
|
|
1451
|
+
*/
|
|
1452
|
+
listCaptures(limit = 100) {
|
|
1453
|
+
if (!(0, import_fs2.existsSync)(this.capturesDir)) {
|
|
1454
|
+
return [];
|
|
1455
|
+
}
|
|
1456
|
+
const files = (0, import_fs2.readdirSync)(this.capturesDir).filter((f) => f.endsWith(".json")).sort().reverse().slice(0, limit);
|
|
1457
|
+
const captures2 = [];
|
|
1458
|
+
for (const file of files) {
|
|
1459
|
+
try {
|
|
1460
|
+
const content = (0, import_fs2.readFileSync)((0, import_path2.join)(this.capturesDir, file), "utf-8");
|
|
1461
|
+
const capture2 = JSON.parse(content);
|
|
1462
|
+
captures2.push({ file, capture: capture2 });
|
|
1463
|
+
} catch {
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return captures2;
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Get a specific capture by ID
|
|
1470
|
+
*/
|
|
1471
|
+
getCapture(captureId) {
|
|
1472
|
+
const captures2 = this.listCaptures(1e3);
|
|
1473
|
+
return captures2.find(
|
|
1474
|
+
(c) => c.capture.id === captureId || c.file.includes(captureId)
|
|
1475
|
+
) || null;
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Delete a capture
|
|
1479
|
+
*/
|
|
1480
|
+
deleteCapture(captureId) {
|
|
1481
|
+
const capture2 = this.getCapture(captureId);
|
|
1482
|
+
if (!capture2) {
|
|
1483
|
+
return false;
|
|
1484
|
+
}
|
|
564
1485
|
try {
|
|
565
|
-
|
|
1486
|
+
(0, import_fs2.unlinkSync)((0, import_path2.join)(this.capturesDir, capture2.file));
|
|
1487
|
+
return true;
|
|
566
1488
|
} catch {
|
|
567
|
-
return
|
|
1489
|
+
return false;
|
|
568
1490
|
}
|
|
569
1491
|
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Get connected client count
|
|
1494
|
+
*/
|
|
1495
|
+
getClientCount() {
|
|
1496
|
+
return this.clients.size;
|
|
1497
|
+
}
|
|
570
1498
|
};
|
|
571
1499
|
|
|
572
|
-
// src/
|
|
573
|
-
var
|
|
574
|
-
|
|
575
|
-
|
|
1500
|
+
// src/commands/capture.ts
|
|
1501
|
+
var capture = new import_commander3.Command().name("capture").description("Start a server to capture incoming webhooks").option("-p, --port <port>", "Port to listen on", "3001").option("-h, --host <host>", "Host to bind to", "0.0.0.0").action(async (options) => {
|
|
1502
|
+
const port = parseInt(options.port, 10);
|
|
1503
|
+
if (isNaN(port) || port < 0 || port > 65535) {
|
|
1504
|
+
console.error(import_chalk3.default.red("Invalid port number"));
|
|
1505
|
+
process.exitCode = 1;
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const server = new CaptureServer();
|
|
1509
|
+
try {
|
|
1510
|
+
await server.start(port, options.host);
|
|
1511
|
+
const shutdown = async () => {
|
|
1512
|
+
await server.stop();
|
|
1513
|
+
process.exit(0);
|
|
1514
|
+
};
|
|
1515
|
+
process.on("SIGINT", shutdown);
|
|
1516
|
+
process.on("SIGTERM", shutdown);
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
console.error(import_chalk3.default.red(`Failed to start server: ${error.message}`));
|
|
1519
|
+
process.exitCode = 1;
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
// src/commands/captures.ts
|
|
1524
|
+
var import_commander4 = require("commander");
|
|
1525
|
+
var import_chalk4 = __toESM(require("chalk"), 1);
|
|
1526
|
+
var import_prompts3 = __toESM(require("prompts"), 1);
|
|
1527
|
+
|
|
1528
|
+
// src/core/replay-engine.ts
|
|
1529
|
+
var import_fs3 = require("fs");
|
|
1530
|
+
var import_path3 = require("path");
|
|
1531
|
+
var import_os3 = require("os");
|
|
1532
|
+
var ReplayEngine = class {
|
|
1533
|
+
capturesDir;
|
|
576
1534
|
constructor(capturesDir) {
|
|
577
|
-
this.capturesDir = capturesDir;
|
|
1535
|
+
this.capturesDir = capturesDir || (0, import_path3.join)((0, import_os3.homedir)(), ".better-webhook", "captures");
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Get the captures directory
|
|
1539
|
+
*/
|
|
1540
|
+
getCapturesDir() {
|
|
1541
|
+
return this.capturesDir;
|
|
578
1542
|
}
|
|
579
1543
|
/**
|
|
580
1544
|
* List all captured webhooks
|
|
581
1545
|
*/
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const files = listJsonFiles(this.capturesDir).sort().reverse();
|
|
585
|
-
return files.map((file) => ({
|
|
586
|
-
file,
|
|
587
|
-
capture: this.loadCapture((0, import_path4.join)(this.capturesDir, file))
|
|
588
|
-
}));
|
|
589
|
-
} catch {
|
|
1546
|
+
listCaptures(limit = 100) {
|
|
1547
|
+
if (!(0, import_fs3.existsSync)(this.capturesDir)) {
|
|
590
1548
|
return [];
|
|
591
1549
|
}
|
|
1550
|
+
const files = (0, import_fs3.readdirSync)(this.capturesDir).filter((f) => f.endsWith(".json")).sort().reverse().slice(0, limit);
|
|
1551
|
+
const captures2 = [];
|
|
1552
|
+
for (const file of files) {
|
|
1553
|
+
try {
|
|
1554
|
+
const content = (0, import_fs3.readFileSync)((0, import_path3.join)(this.capturesDir, file), "utf-8");
|
|
1555
|
+
const capture2 = JSON.parse(content);
|
|
1556
|
+
captures2.push({ file, capture: capture2 });
|
|
1557
|
+
} catch {
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return captures2;
|
|
592
1561
|
}
|
|
593
1562
|
/**
|
|
594
|
-
*
|
|
1563
|
+
* Get a specific capture by ID or partial filename
|
|
595
1564
|
*/
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1565
|
+
getCapture(captureId) {
|
|
1566
|
+
const captures2 = this.listCaptures(1e3);
|
|
1567
|
+
let found = captures2.find((c) => c.capture.id === captureId);
|
|
1568
|
+
if (found) return found;
|
|
1569
|
+
found = captures2.find((c) => c.file.includes(captureId));
|
|
1570
|
+
if (found) return found;
|
|
1571
|
+
found = captures2.find((c) => c.capture.id.startsWith(captureId));
|
|
1572
|
+
return found || null;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Replay a captured webhook
|
|
1576
|
+
*/
|
|
1577
|
+
async replay(captureId, options) {
|
|
1578
|
+
const captureFile = this.getCapture(captureId);
|
|
1579
|
+
if (!captureFile) {
|
|
1580
|
+
throw new Error(`Capture not found: ${captureId}`);
|
|
1581
|
+
}
|
|
1582
|
+
const { capture: capture2 } = captureFile;
|
|
1583
|
+
const headers = [];
|
|
1584
|
+
const skipHeaders = [
|
|
1585
|
+
"host",
|
|
1586
|
+
"content-length",
|
|
1587
|
+
"connection",
|
|
1588
|
+
"accept-encoding"
|
|
1589
|
+
];
|
|
1590
|
+
for (const [key, value] of Object.entries(capture2.headers)) {
|
|
1591
|
+
if (!skipHeaders.includes(key.toLowerCase())) {
|
|
1592
|
+
const headerValue = Array.isArray(value) ? value.join(", ") : value;
|
|
1593
|
+
if (headerValue) {
|
|
1594
|
+
headers.push({ key, value: headerValue });
|
|
1595
|
+
}
|
|
606
1596
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
1597
|
+
}
|
|
1598
|
+
if (options.headers) {
|
|
1599
|
+
for (const h of options.headers) {
|
|
1600
|
+
const existingIdx = headers.findIndex(
|
|
1601
|
+
(eh) => eh.key.toLowerCase() === h.key.toLowerCase()
|
|
610
1602
|
);
|
|
1603
|
+
if (existingIdx >= 0) {
|
|
1604
|
+
headers[existingIdx] = h;
|
|
1605
|
+
} else {
|
|
1606
|
+
headers.push(h);
|
|
1607
|
+
}
|
|
611
1608
|
}
|
|
612
|
-
filepath = (0, import_path4.join)(this.capturesDir, files[0] ?? "");
|
|
613
1609
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
1610
|
+
const body = capture2.rawBody || capture2.body;
|
|
1611
|
+
return executeWebhook({
|
|
1612
|
+
url: options.targetUrl,
|
|
1613
|
+
method: options.method || capture2.method,
|
|
1614
|
+
headers,
|
|
1615
|
+
body
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Convert a capture to a template
|
|
1620
|
+
*/
|
|
1621
|
+
captureToTemplate(captureId, options) {
|
|
1622
|
+
const captureFile = this.getCapture(captureId);
|
|
1623
|
+
if (!captureFile) {
|
|
1624
|
+
throw new Error(`Capture not found: ${captureId}`);
|
|
1625
|
+
}
|
|
1626
|
+
const { capture: capture2 } = captureFile;
|
|
1627
|
+
const skipHeaders = [
|
|
1628
|
+
"host",
|
|
1629
|
+
"content-length",
|
|
1630
|
+
"connection",
|
|
1631
|
+
"accept-encoding",
|
|
1632
|
+
"stripe-signature",
|
|
1633
|
+
"x-hub-signature-256",
|
|
1634
|
+
"x-hub-signature",
|
|
1635
|
+
"x-shopify-hmac-sha256",
|
|
1636
|
+
"x-twilio-signature",
|
|
1637
|
+
"x-slack-signature",
|
|
1638
|
+
"svix-signature",
|
|
1639
|
+
"linear-signature"
|
|
1640
|
+
];
|
|
1641
|
+
const headers = [];
|
|
1642
|
+
for (const [key, value] of Object.entries(capture2.headers)) {
|
|
1643
|
+
if (!skipHeaders.includes(key.toLowerCase())) {
|
|
1644
|
+
const headerValue = Array.isArray(value) ? value.join(", ") : value;
|
|
1645
|
+
if (headerValue) {
|
|
1646
|
+
headers.push({ key, value: headerValue });
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
let body;
|
|
1651
|
+
if (capture2.body) {
|
|
1652
|
+
body = capture2.body;
|
|
1653
|
+
} else if (capture2.rawBody) {
|
|
1654
|
+
try {
|
|
1655
|
+
body = JSON.parse(capture2.rawBody);
|
|
1656
|
+
} catch {
|
|
1657
|
+
body = capture2.rawBody;
|
|
1658
|
+
}
|
|
621
1659
|
}
|
|
1660
|
+
return {
|
|
1661
|
+
url: options?.url || `http://localhost:3000${capture2.path}`,
|
|
1662
|
+
method: capture2.method,
|
|
1663
|
+
headers,
|
|
1664
|
+
body,
|
|
1665
|
+
provider: capture2.provider,
|
|
1666
|
+
description: `Captured ${capture2.provider || "webhook"} at ${capture2.timestamp}`
|
|
1667
|
+
};
|
|
622
1668
|
}
|
|
623
1669
|
/**
|
|
624
|
-
*
|
|
1670
|
+
* Get a summary of a capture
|
|
625
1671
|
*/
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
method: options.method || capture2.method,
|
|
631
|
-
headers: options.headers || this.convertHeaders(capture2.headers),
|
|
632
|
-
body: capture2.body
|
|
633
|
-
};
|
|
634
|
-
console.log(`\u{1F504} Replaying webhook ${capture2.id} (${capture2.timestamp})`);
|
|
635
|
-
console.log(` Method: ${webhookDef.method}`);
|
|
636
|
-
console.log(` URL: ${webhookDef.url}`);
|
|
637
|
-
console.log(` Original: ${capture2.method} ${capture2.url}`);
|
|
638
|
-
try {
|
|
639
|
-
const result = await executeWebhook(webhookDef);
|
|
640
|
-
return result;
|
|
641
|
-
} catch (error) {
|
|
642
|
-
throw new Error(`Replay failed: ${error.message}`);
|
|
1672
|
+
getCaptureSummary(captureId) {
|
|
1673
|
+
const captureFile = this.getCapture(captureId);
|
|
1674
|
+
if (!captureFile) {
|
|
1675
|
+
return "Capture not found";
|
|
643
1676
|
}
|
|
1677
|
+
const { capture: capture2 } = captureFile;
|
|
1678
|
+
const lines = [];
|
|
1679
|
+
lines.push(`ID: ${capture2.id}`);
|
|
1680
|
+
lines.push(`Timestamp: ${new Date(capture2.timestamp).toLocaleString()}`);
|
|
1681
|
+
lines.push(`Method: ${capture2.method}`);
|
|
1682
|
+
lines.push(`Path: ${capture2.path}`);
|
|
1683
|
+
if (capture2.provider) {
|
|
1684
|
+
lines.push(`Provider: ${capture2.provider}`);
|
|
1685
|
+
}
|
|
1686
|
+
lines.push(`Content-Type: ${capture2.contentType || "unknown"}`);
|
|
1687
|
+
lines.push(`Body Size: ${capture2.contentLength || 0} bytes`);
|
|
1688
|
+
const headerCount = Object.keys(capture2.headers).length;
|
|
1689
|
+
lines.push(`Headers: ${headerCount}`);
|
|
1690
|
+
return lines.join("\n");
|
|
644
1691
|
}
|
|
645
1692
|
/**
|
|
646
|
-
*
|
|
1693
|
+
* Search captures
|
|
647
1694
|
*/
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
};
|
|
1695
|
+
searchCaptures(query) {
|
|
1696
|
+
const queryLower = query.toLowerCase();
|
|
1697
|
+
const captures2 = this.listCaptures(1e3);
|
|
1698
|
+
return captures2.filter((c) => {
|
|
1699
|
+
const { capture: capture2 } = c;
|
|
1700
|
+
return capture2.id.toLowerCase().includes(queryLower) || capture2.path.toLowerCase().includes(queryLower) || capture2.method.toLowerCase().includes(queryLower) || capture2.provider?.toLowerCase().includes(queryLower) || c.file.toLowerCase().includes(queryLower);
|
|
1701
|
+
});
|
|
656
1702
|
}
|
|
657
1703
|
/**
|
|
658
|
-
*
|
|
1704
|
+
* Get captures by provider
|
|
659
1705
|
*/
|
|
660
|
-
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
} else {
|
|
679
|
-
result.push({ key, value: value.join(", ") });
|
|
680
|
-
}
|
|
681
|
-
} else {
|
|
682
|
-
result.push({ key, value });
|
|
683
|
-
}
|
|
1706
|
+
getCapturesByProvider(provider) {
|
|
1707
|
+
const captures2 = this.listCaptures(1e3);
|
|
1708
|
+
return captures2.filter((c) => c.capture.provider === provider);
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Delete a specific capture by ID
|
|
1712
|
+
* @returns true if deleted, false if not found
|
|
1713
|
+
*/
|
|
1714
|
+
deleteCapture(captureId) {
|
|
1715
|
+
const captureFile = this.getCapture(captureId);
|
|
1716
|
+
if (!captureFile) {
|
|
1717
|
+
return false;
|
|
1718
|
+
}
|
|
1719
|
+
try {
|
|
1720
|
+
(0, import_fs3.unlinkSync)((0, import_path3.join)(this.capturesDir, captureFile.file));
|
|
1721
|
+
return true;
|
|
1722
|
+
} catch {
|
|
1723
|
+
return false;
|
|
684
1724
|
}
|
|
685
|
-
return result;
|
|
686
1725
|
}
|
|
687
1726
|
/**
|
|
688
|
-
*
|
|
1727
|
+
* Delete all captures
|
|
1728
|
+
* @returns Number of captures deleted
|
|
689
1729
|
*/
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1730
|
+
deleteAllCaptures() {
|
|
1731
|
+
if (!(0, import_fs3.existsSync)(this.capturesDir)) {
|
|
1732
|
+
return 0;
|
|
1733
|
+
}
|
|
1734
|
+
const files = (0, import_fs3.readdirSync)(this.capturesDir).filter(
|
|
1735
|
+
(f) => f.endsWith(".json")
|
|
1736
|
+
);
|
|
1737
|
+
let deleted = 0;
|
|
1738
|
+
for (const file of files) {
|
|
1739
|
+
try {
|
|
1740
|
+
(0, import_fs3.unlinkSync)((0, import_path3.join)(this.capturesDir, file));
|
|
1741
|
+
deleted++;
|
|
1742
|
+
} catch {
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return deleted;
|
|
704
1746
|
}
|
|
705
1747
|
};
|
|
1748
|
+
var instance2 = null;
|
|
1749
|
+
function getReplayEngine(capturesDir) {
|
|
1750
|
+
if (!instance2) {
|
|
1751
|
+
instance2 = new ReplayEngine(capturesDir);
|
|
1752
|
+
}
|
|
1753
|
+
return instance2;
|
|
1754
|
+
}
|
|
706
1755
|
|
|
707
|
-
// src/commands/
|
|
708
|
-
var listCommand2 = new
|
|
709
|
-
const cwd = process.cwd();
|
|
710
|
-
const capturesDir = findCapturesDir(cwd);
|
|
711
|
-
const replayer = new WebhookReplayer(capturesDir);
|
|
712
|
-
const captures = replayer.listCaptured();
|
|
1756
|
+
// src/commands/captures.ts
|
|
1757
|
+
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) => {
|
|
713
1758
|
const limit = parseInt(options.limit, 10);
|
|
714
|
-
if (
|
|
715
|
-
console.error("Invalid
|
|
1759
|
+
if (isNaN(limit) || limit <= 0) {
|
|
1760
|
+
console.error(import_chalk4.default.red("Invalid limit value"));
|
|
716
1761
|
process.exitCode = 1;
|
|
717
1762
|
return;
|
|
718
1763
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1764
|
+
const engine = getReplayEngine();
|
|
1765
|
+
let captures2 = engine.listCaptures(limit);
|
|
1766
|
+
if (options.provider) {
|
|
1767
|
+
captures2 = captures2.filter(
|
|
1768
|
+
(c) => c.capture.provider?.toLowerCase() === options.provider?.toLowerCase()
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
if (captures2.length === 0) {
|
|
1772
|
+
console.log(import_chalk4.default.yellow("\n\u{1F4ED} No captured webhooks found."));
|
|
1773
|
+
console.log(
|
|
1774
|
+
import_chalk4.default.gray(" Start capturing with: better-webhook capture\n")
|
|
1775
|
+
);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
console.log(import_chalk4.default.bold("\n\u{1F4E6} Captured Webhooks\n"));
|
|
1779
|
+
for (const { file, capture: capture2 } of captures2) {
|
|
1780
|
+
const date = new Date(capture2.timestamp).toLocaleString();
|
|
1781
|
+
const provider = capture2.provider ? import_chalk4.default.cyan(`[${capture2.provider}]`) : import_chalk4.default.gray("[unknown]");
|
|
1782
|
+
const size = capture2.contentLength || capture2.rawBody?.length || 0;
|
|
1783
|
+
console.log(` ${import_chalk4.default.white(capture2.id.slice(0, 8))} ${provider}`);
|
|
1784
|
+
console.log(import_chalk4.default.gray(` ${capture2.method} ${capture2.path}`));
|
|
1785
|
+
console.log(import_chalk4.default.gray(` ${date} | ${size} bytes`));
|
|
1786
|
+
console.log(import_chalk4.default.gray(` File: ${file}`));
|
|
1787
|
+
console.log();
|
|
1788
|
+
}
|
|
1789
|
+
console.log(import_chalk4.default.gray(` Showing ${captures2.length} captures`));
|
|
1790
|
+
console.log(import_chalk4.default.gray(` Storage: ${engine.getCapturesDir()}
|
|
1791
|
+
`));
|
|
1792
|
+
});
|
|
1793
|
+
var showCommand = new import_commander4.Command().name("show").argument("<captureId>", "Capture ID or partial ID").description("Show detailed information about a capture").option("-b, --body", "Show full body content").action((captureId, options) => {
|
|
1794
|
+
const engine = getReplayEngine();
|
|
1795
|
+
const captureFile = engine.getCapture(captureId);
|
|
1796
|
+
if (!captureFile) {
|
|
1797
|
+
console.log(import_chalk4.default.red(`
|
|
1798
|
+
\u274C Capture not found: ${captureId}
|
|
1799
|
+
`));
|
|
1800
|
+
process.exitCode = 1;
|
|
722
1801
|
return;
|
|
723
1802
|
}
|
|
1803
|
+
const { capture: capture2 } = captureFile;
|
|
1804
|
+
console.log(import_chalk4.default.bold("\n\u{1F4CB} Capture Details\n"));
|
|
1805
|
+
console.log(` ${import_chalk4.default.gray("ID:")} ${capture2.id}`);
|
|
1806
|
+
console.log(` ${import_chalk4.default.gray("File:")} ${captureFile.file}`);
|
|
724
1807
|
console.log(
|
|
725
|
-
|
|
726
|
-
`
|
|
1808
|
+
` ${import_chalk4.default.gray("Timestamp:")} ${new Date(capture2.timestamp).toLocaleString()}`
|
|
727
1809
|
);
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
console.log(` ${capture2.method} ${capture2.url} | ${bodySize} bytes`);
|
|
733
|
-
console.log(` \u{1F4C4} ${file}
|
|
734
|
-
`);
|
|
735
|
-
});
|
|
736
|
-
if (captures.length > limit) {
|
|
1810
|
+
console.log(` ${import_chalk4.default.gray("Method:")} ${capture2.method}`);
|
|
1811
|
+
console.log(` ${import_chalk4.default.gray("Path:")} ${capture2.path}`);
|
|
1812
|
+
console.log(` ${import_chalk4.default.gray("URL:")} ${capture2.url}`);
|
|
1813
|
+
if (capture2.provider) {
|
|
737
1814
|
console.log(
|
|
738
|
-
|
|
1815
|
+
` ${import_chalk4.default.gray("Provider:")} ${import_chalk4.default.cyan(capture2.provider)}`
|
|
739
1816
|
);
|
|
740
1817
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1818
|
+
console.log(
|
|
1819
|
+
` ${import_chalk4.default.gray("Content-Type:")} ${capture2.contentType || "unknown"}`
|
|
1820
|
+
);
|
|
1821
|
+
console.log(
|
|
1822
|
+
` ${import_chalk4.default.gray("Content-Length:")} ${capture2.contentLength || 0} bytes`
|
|
1823
|
+
);
|
|
1824
|
+
const queryKeys = Object.keys(capture2.query);
|
|
1825
|
+
if (queryKeys.length > 0) {
|
|
1826
|
+
console.log(import_chalk4.default.bold("\n Query Parameters:"));
|
|
1827
|
+
for (const [key, value] of Object.entries(capture2.query)) {
|
|
1828
|
+
const queryValue = Array.isArray(value) ? value.join(", ") : value;
|
|
1829
|
+
console.log(import_chalk4.default.gray(` ${key}: ${queryValue}`));
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
console.log(import_chalk4.default.bold("\n Headers:"));
|
|
1833
|
+
for (const [key, value] of Object.entries(capture2.headers)) {
|
|
1834
|
+
const headerValue = Array.isArray(value) ? value.join(", ") : value;
|
|
1835
|
+
const display = headerValue.length > 80 ? headerValue.slice(0, 80) + "..." : headerValue;
|
|
1836
|
+
console.log(import_chalk4.default.gray(` ${key}: ${display}`));
|
|
1837
|
+
}
|
|
1838
|
+
if (options.body && capture2.body) {
|
|
1839
|
+
console.log(import_chalk4.default.bold("\n Body:"));
|
|
1840
|
+
if (typeof capture2.body === "object") {
|
|
763
1841
|
console.log(
|
|
764
|
-
|
|
1842
|
+
import_chalk4.default.gray(
|
|
1843
|
+
JSON.stringify(capture2.body, null, 2).split("\n").map((l) => ` ${l}`).join("\n")
|
|
1844
|
+
)
|
|
765
1845
|
);
|
|
766
|
-
|
|
767
|
-
console.log(
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
|
|
1846
|
+
} else {
|
|
1847
|
+
console.log(import_chalk4.default.gray(` ${capture2.body}`));
|
|
1848
|
+
}
|
|
1849
|
+
} else if (capture2.body) {
|
|
1850
|
+
console.log(import_chalk4.default.bold("\n Body:"));
|
|
1851
|
+
const preview = JSON.stringify(capture2.body).slice(0, 200);
|
|
1852
|
+
console.log(
|
|
1853
|
+
import_chalk4.default.gray(` ${preview}${preview.length >= 200 ? "..." : ""}`)
|
|
1854
|
+
);
|
|
1855
|
+
console.log(import_chalk4.default.gray(" Use --body to see full content"));
|
|
1856
|
+
}
|
|
1857
|
+
console.log();
|
|
1858
|
+
});
|
|
1859
|
+
var searchCommand2 = new import_commander4.Command().name("search").argument("<query>", "Search query").description("Search captures by path, method, or provider").action((query) => {
|
|
1860
|
+
const engine = getReplayEngine();
|
|
1861
|
+
const results = engine.searchCaptures(query);
|
|
1862
|
+
if (results.length === 0) {
|
|
1863
|
+
console.log(import_chalk4.default.yellow(`
|
|
1864
|
+
\u{1F4ED} No captures found for: "${query}"
|
|
1865
|
+
`));
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
console.log(import_chalk4.default.bold(`
|
|
1869
|
+
\u{1F50D} Search Results for "${query}"
|
|
1870
|
+
`));
|
|
1871
|
+
for (const { file, capture: capture2 } of results) {
|
|
1872
|
+
const date = new Date(capture2.timestamp).toLocaleString();
|
|
1873
|
+
const provider = capture2.provider ? import_chalk4.default.cyan(`[${capture2.provider}]`) : "";
|
|
1874
|
+
console.log(` ${import_chalk4.default.white(capture2.id.slice(0, 8))} ${provider}`);
|
|
1875
|
+
console.log(import_chalk4.default.gray(` ${capture2.method} ${capture2.path}`));
|
|
1876
|
+
console.log(import_chalk4.default.gray(` ${date}`));
|
|
1877
|
+
console.log();
|
|
1878
|
+
}
|
|
1879
|
+
console.log(import_chalk4.default.gray(` Found: ${results.length} captures
|
|
1880
|
+
`));
|
|
1881
|
+
});
|
|
1882
|
+
var deleteCommand = new import_commander4.Command().name("delete").alias("rm").argument("<captureId>", "Capture ID or partial ID to delete").description("Delete a specific captured webhook").option("-f, --force", "Skip confirmation prompt").action(async (captureId, options) => {
|
|
1883
|
+
const engine = getReplayEngine();
|
|
1884
|
+
const captureFile = engine.getCapture(captureId);
|
|
1885
|
+
if (!captureFile) {
|
|
1886
|
+
console.log(import_chalk4.default.red(`
|
|
1887
|
+
\u274C Capture not found: ${captureId}
|
|
1888
|
+
`));
|
|
1889
|
+
process.exitCode = 1;
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
const { capture: capture2 } = captureFile;
|
|
1893
|
+
if (!options.force) {
|
|
1894
|
+
console.log(import_chalk4.default.bold("\n\u{1F5D1}\uFE0F Capture to delete:\n"));
|
|
1895
|
+
console.log(` ${import_chalk4.default.white(capture2.id.slice(0, 8))}`);
|
|
1896
|
+
console.log(import_chalk4.default.gray(` ${capture2.method} ${capture2.path}`));
|
|
1897
|
+
console.log(
|
|
1898
|
+
import_chalk4.default.gray(` ${new Date(capture2.timestamp).toLocaleString()}`)
|
|
1899
|
+
);
|
|
1900
|
+
console.log();
|
|
1901
|
+
const response = await (0, import_prompts3.default)({
|
|
1902
|
+
type: "confirm",
|
|
1903
|
+
name: "confirm",
|
|
1904
|
+
message: "Delete this capture?",
|
|
1905
|
+
initial: false
|
|
1906
|
+
});
|
|
1907
|
+
if (!response.confirm) {
|
|
1908
|
+
console.log(import_chalk4.default.yellow("Cancelled"));
|
|
1909
|
+
return;
|
|
771
1910
|
}
|
|
772
1911
|
}
|
|
773
|
-
);
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
console.error("Failed to start capture server:", error.message);
|
|
1912
|
+
const deleted = engine.deleteCapture(captureId);
|
|
1913
|
+
if (deleted) {
|
|
1914
|
+
console.log(
|
|
1915
|
+
import_chalk4.default.green(`
|
|
1916
|
+
\u2713 Deleted capture: ${capture2.id.slice(0, 8)}
|
|
1917
|
+
`)
|
|
1918
|
+
);
|
|
1919
|
+
} else {
|
|
1920
|
+
console.log(import_chalk4.default.red(`
|
|
1921
|
+
\u274C Failed to delete capture
|
|
1922
|
+
`));
|
|
785
1923
|
process.exitCode = 1;
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
var cleanCommand2 = new import_commander4.Command().name("clean").alias("remove-all").description("Remove all captured webhooks").option("-f, --force", "Skip confirmation prompt").action(async (options) => {
|
|
1927
|
+
const engine = getReplayEngine();
|
|
1928
|
+
const captures2 = engine.listCaptures(1e4);
|
|
1929
|
+
if (captures2.length === 0) {
|
|
1930
|
+
console.log(import_chalk4.default.yellow("\n\u{1F4ED} No captures to remove.\n"));
|
|
786
1931
|
return;
|
|
787
1932
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1933
|
+
console.log(
|
|
1934
|
+
import_chalk4.default.bold(`
|
|
1935
|
+
\u{1F5D1}\uFE0F Found ${captures2.length} captured webhook(s)
|
|
1936
|
+
`)
|
|
1937
|
+
);
|
|
1938
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
1939
|
+
for (const c of captures2) {
|
|
1940
|
+
const provider = c.capture.provider || "unknown";
|
|
1941
|
+
byProvider.set(provider, (byProvider.get(provider) || 0) + 1);
|
|
1942
|
+
}
|
|
1943
|
+
for (const [provider, count] of byProvider) {
|
|
1944
|
+
console.log(import_chalk4.default.gray(` ${provider}: ${count}`));
|
|
1945
|
+
}
|
|
1946
|
+
console.log();
|
|
1947
|
+
if (!options.force) {
|
|
1948
|
+
const response = await (0, import_prompts3.default)({
|
|
1949
|
+
type: "confirm",
|
|
1950
|
+
name: "confirm",
|
|
1951
|
+
message: `Delete all ${captures2.length} capture(s)?`,
|
|
1952
|
+
initial: false
|
|
1953
|
+
});
|
|
1954
|
+
if (!response.confirm) {
|
|
1955
|
+
console.log(import_chalk4.default.yellow("Cancelled"));
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
const deleted = engine.deleteAllCaptures();
|
|
1960
|
+
console.log(import_chalk4.default.green(`
|
|
1961
|
+
\u2713 Removed ${deleted} capture(s)`));
|
|
1962
|
+
console.log(import_chalk4.default.gray(` Storage: ${engine.getCapturesDir()}
|
|
1963
|
+
`));
|
|
1964
|
+
});
|
|
1965
|
+
var captures = new import_commander4.Command().name("captures").alias("c").description("Manage captured webhooks").addCommand(listCommand2).addCommand(showCommand).addCommand(searchCommand2).addCommand(deleteCommand).addCommand(cleanCommand2);
|
|
796
1966
|
|
|
797
1967
|
// src/commands/replay.ts
|
|
798
|
-
var
|
|
799
|
-
var
|
|
800
|
-
var
|
|
801
|
-
var
|
|
802
|
-
|
|
803
|
-
"ID of the captured webhook to replay (optional - will prompt if not provided)"
|
|
804
|
-
).argument(
|
|
805
|
-
"[targetUrl]",
|
|
806
|
-
"Target URL to replay the webhook to (optional - will prompt if not provided)"
|
|
807
|
-
).description("Replay a captured webhook to a target URL").option("-m, --method <method>", "Override HTTP method").option(
|
|
1968
|
+
var import_commander5 = require("commander");
|
|
1969
|
+
var import_ora3 = __toESM(require("ora"), 1);
|
|
1970
|
+
var import_prompts4 = __toESM(require("prompts"), 1);
|
|
1971
|
+
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
1972
|
+
var replay = new import_commander5.Command().name("replay").argument("[captureId]", "Capture ID to replay").argument("[targetUrl]", "Target URL to replay to").description("Replay a captured webhook to a target URL").option("-m, --method <method>", "Override HTTP method").option(
|
|
808
1973
|
"-H, --header <header>",
|
|
809
|
-
"Add
|
|
1974
|
+
"Add or override header (format: key:value)",
|
|
810
1975
|
(value, previous) => {
|
|
811
1976
|
const [key, ...valueParts] = value.split(":");
|
|
812
1977
|
const headerValue = valueParts.join(":");
|
|
@@ -818,53 +1983,53 @@ var replay = new import_commander3.Command().name("replay").argument(
|
|
|
818
1983
|
]);
|
|
819
1984
|
},
|
|
820
1985
|
[]
|
|
821
|
-
).action(
|
|
822
|
-
async (captureId, targetUrl, options
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
)
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
let selectedCaptureId = captureId;
|
|
837
|
-
let selectedTargetUrl = targetUrl;
|
|
838
|
-
if (!selectedCaptureId) {
|
|
839
|
-
const choices = captured.map((c) => {
|
|
1986
|
+
).option("-v, --verbose", "Show detailed request/response information").action(
|
|
1987
|
+
async (captureId, targetUrl, options) => {
|
|
1988
|
+
const engine = getReplayEngine();
|
|
1989
|
+
if (!captureId) {
|
|
1990
|
+
const captures2 = engine.listCaptures(50);
|
|
1991
|
+
if (captures2.length === 0) {
|
|
1992
|
+
console.log(import_chalk5.default.yellow("\n\u{1F4ED} No captured webhooks found."));
|
|
1993
|
+
console.log(
|
|
1994
|
+
import_chalk5.default.gray(" Start capturing with: better-webhook capture\n")
|
|
1995
|
+
);
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
const choices = captures2.map((c) => {
|
|
840
1999
|
const date = new Date(c.capture.timestamp).toLocaleString();
|
|
841
|
-
const
|
|
2000
|
+
const provider = c.capture.provider ? `[${c.capture.provider}]` : "";
|
|
842
2001
|
return {
|
|
843
|
-
title: `${c.capture.id}
|
|
844
|
-
description:
|
|
2002
|
+
title: `${c.capture.id.slice(0, 8)} ${provider} ${c.capture.method} ${c.capture.path}`,
|
|
2003
|
+
description: date,
|
|
845
2004
|
value: c.capture.id
|
|
846
2005
|
};
|
|
847
2006
|
});
|
|
848
|
-
const response = await (0,
|
|
2007
|
+
const response = await (0, import_prompts4.default)({
|
|
849
2008
|
type: "select",
|
|
850
2009
|
name: "captureId",
|
|
851
|
-
message: "Select a
|
|
852
|
-
choices
|
|
853
|
-
initial: 0
|
|
2010
|
+
message: "Select a capture to replay:",
|
|
2011
|
+
choices
|
|
854
2012
|
});
|
|
855
2013
|
if (!response.captureId) {
|
|
856
|
-
console.log(
|
|
857
|
-
process.exitCode = 1;
|
|
2014
|
+
console.log(import_chalk5.default.yellow("Cancelled"));
|
|
858
2015
|
return;
|
|
859
2016
|
}
|
|
860
|
-
|
|
2017
|
+
captureId = response.captureId;
|
|
2018
|
+
}
|
|
2019
|
+
const captureFile = engine.getCapture(captureId);
|
|
2020
|
+
if (!captureFile) {
|
|
2021
|
+
console.log(import_chalk5.default.red(`
|
|
2022
|
+
\u274C Capture not found: ${captureId}
|
|
2023
|
+
`));
|
|
2024
|
+
process.exitCode = 1;
|
|
2025
|
+
return;
|
|
861
2026
|
}
|
|
862
|
-
if (!
|
|
863
|
-
const response = await (0,
|
|
2027
|
+
if (!targetUrl) {
|
|
2028
|
+
const response = await (0, import_prompts4.default)({
|
|
864
2029
|
type: "text",
|
|
865
|
-
name: "
|
|
866
|
-
message: "Enter
|
|
867
|
-
initial:
|
|
2030
|
+
name: "url",
|
|
2031
|
+
message: "Enter target URL:",
|
|
2032
|
+
initial: `http://localhost:3000${captureFile.capture.path}`,
|
|
868
2033
|
validate: (value) => {
|
|
869
2034
|
try {
|
|
870
2035
|
new URL(value);
|
|
@@ -874,43 +2039,550 @@ var replay = new import_commander3.Command().name("replay").argument(
|
|
|
874
2039
|
}
|
|
875
2040
|
}
|
|
876
2041
|
});
|
|
877
|
-
if (!response.
|
|
878
|
-
console.log(
|
|
879
|
-
process.exitCode = 1;
|
|
2042
|
+
if (!response.url) {
|
|
2043
|
+
console.log(import_chalk5.default.yellow("Cancelled"));
|
|
880
2044
|
return;
|
|
881
2045
|
}
|
|
882
|
-
|
|
2046
|
+
targetUrl = response.url;
|
|
883
2047
|
}
|
|
2048
|
+
const { capture: capture2 } = captureFile;
|
|
2049
|
+
console.log(import_chalk5.default.bold("\n\u{1F504} Replaying Webhook\n"));
|
|
2050
|
+
console.log(import_chalk5.default.gray(` Capture ID: ${capture2.id.slice(0, 8)}`));
|
|
2051
|
+
console.log(import_chalk5.default.gray(` Original: ${capture2.method} ${capture2.path}`));
|
|
2052
|
+
if (capture2.provider) {
|
|
2053
|
+
console.log(import_chalk5.default.gray(` Provider: ${capture2.provider}`));
|
|
2054
|
+
}
|
|
2055
|
+
console.log(import_chalk5.default.gray(` Target: ${targetUrl}`));
|
|
2056
|
+
console.log();
|
|
2057
|
+
const spinner = (0, import_ora3.default)("Replaying webhook...").start();
|
|
884
2058
|
try {
|
|
885
|
-
const result = await
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
2059
|
+
const result = await engine.replay(captureId, {
|
|
2060
|
+
targetUrl,
|
|
2061
|
+
method: options?.method,
|
|
2062
|
+
headers: options?.header
|
|
2063
|
+
});
|
|
2064
|
+
spinner.stop();
|
|
2065
|
+
const statusColor = result.status >= 200 && result.status < 300 ? import_chalk5.default.green : result.status >= 400 ? import_chalk5.default.red : import_chalk5.default.yellow;
|
|
2066
|
+
console.log(import_chalk5.default.bold("\u{1F4E5} Response\n"));
|
|
2067
|
+
console.log(
|
|
2068
|
+
` Status: ${statusColor(`${result.status} ${result.statusText}`)}`
|
|
892
2069
|
);
|
|
893
|
-
console.log(
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
2070
|
+
console.log(` Duration: ${import_chalk5.default.cyan(`${result.duration}ms`)}`);
|
|
2071
|
+
if (options?.verbose) {
|
|
2072
|
+
console.log(import_chalk5.default.bold("\n Headers:"));
|
|
2073
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
2074
|
+
const headerValue = Array.isArray(value) ? value.join(", ") : value;
|
|
2075
|
+
console.log(import_chalk5.default.gray(` ${key}: ${headerValue}`));
|
|
2076
|
+
}
|
|
898
2077
|
}
|
|
899
2078
|
if (result.json !== void 0) {
|
|
900
|
-
console.log("
|
|
901
|
-
console.log(
|
|
2079
|
+
console.log(import_chalk5.default.bold("\n Body:"));
|
|
2080
|
+
console.log(
|
|
2081
|
+
import_chalk5.default.gray(
|
|
2082
|
+
JSON.stringify(result.json, null, 2).split("\n").map((l) => ` ${l}`).join("\n")
|
|
2083
|
+
)
|
|
2084
|
+
);
|
|
2085
|
+
} else if (result.bodyText) {
|
|
2086
|
+
console.log(import_chalk5.default.bold("\n Body:"));
|
|
2087
|
+
const preview = result.bodyText.length > 500 ? result.bodyText.slice(0, 500) + "..." : result.bodyText;
|
|
2088
|
+
console.log(import_chalk5.default.gray(` ${preview}`));
|
|
2089
|
+
}
|
|
2090
|
+
console.log();
|
|
2091
|
+
if (result.status >= 200 && result.status < 300) {
|
|
2092
|
+
console.log(import_chalk5.default.green("\u2713 Replay completed successfully\n"));
|
|
902
2093
|
} else {
|
|
903
|
-
console.log(
|
|
904
|
-
|
|
2094
|
+
console.log(
|
|
2095
|
+
import_chalk5.default.yellow(`\u26A0 Replay completed with status ${result.status}
|
|
2096
|
+
`)
|
|
2097
|
+
);
|
|
905
2098
|
}
|
|
906
2099
|
} catch (error) {
|
|
907
|
-
|
|
2100
|
+
spinner.fail("Replay failed");
|
|
2101
|
+
console.error(import_chalk5.default.red(`
|
|
2102
|
+
\u274C ${error.message}
|
|
2103
|
+
`));
|
|
908
2104
|
process.exitCode = 1;
|
|
909
2105
|
}
|
|
910
2106
|
}
|
|
911
2107
|
);
|
|
912
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
|
+
|
|
913
2583
|
// src/index.ts
|
|
914
|
-
var program = new
|
|
915
|
-
|
|
2584
|
+
var program = new import_commander7.Command().name("better-webhook").description(
|
|
2585
|
+
"Modern CLI for developing, capturing, and replaying webhooks locally"
|
|
2586
|
+
).version("2.0.0");
|
|
2587
|
+
program.addCommand(templates).addCommand(run).addCommand(capture).addCommand(captures).addCommand(replay).addCommand(dashboard);
|
|
916
2588
|
program.parseAsync(process.argv);
|