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