@better-webhook/cli 3.4.4 ā 3.6.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 +66 -0
- package/dist/dashboard/assets/index-CxrRCNTh.css +1 -0
- package/dist/dashboard/assets/index-Dlqdzwyc.js +23 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/index.cjs +447 -33
- package/dist/index.js +448 -34
- package/package.json +7 -3
- package/dist/commands/capture.d.ts +0 -2
- package/dist/commands/capture.js +0 -30
- package/dist/commands/captures.d.ts +0 -2
- package/dist/commands/captures.js +0 -217
- package/dist/commands/dashboard.d.ts +0 -2
- package/dist/commands/dashboard.js +0 -65
- package/dist/commands/index.d.ts +0 -6
- package/dist/commands/index.js +0 -6
- package/dist/commands/replay.d.ts +0 -2
- package/dist/commands/replay.js +0 -140
- package/dist/commands/run.d.ts +0 -2
- package/dist/commands/run.js +0 -181
- package/dist/commands/templates.d.ts +0 -2
- package/dist/commands/templates.js +0 -285
- package/dist/core/capture-server.d.ts +0 -31
- package/dist/core/capture-server.js +0 -298
- package/dist/core/dashboard-api.d.ts +0 -8
- package/dist/core/dashboard-api.js +0 -271
- package/dist/core/dashboard-server.d.ts +0 -20
- package/dist/core/dashboard-server.js +0 -124
- package/dist/core/executor.d.ts +0 -11
- package/dist/core/executor.js +0 -130
- package/dist/core/index.d.ts +0 -5
- package/dist/core/index.js +0 -5
- package/dist/core/replay-engine.d.ts +0 -18
- package/dist/core/replay-engine.js +0 -208
- package/dist/core/signature.d.ts +0 -24
- package/dist/core/signature.js +0 -199
- package/dist/core/template-manager.d.ts +0 -24
- package/dist/core/template-manager.js +0 -246
- package/dist/dashboard/assets/index-BSfTbn4Y.js +0 -23
- package/dist/dashboard/assets/index-zDTVdss_.css +0 -1
- package/dist/types/index.d.ts +0 -299
- package/dist/types/index.js +0 -86
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import ora from "ora";
|
|
3
|
-
import prompts from "prompts";
|
|
4
|
-
import chalk from "chalk";
|
|
5
|
-
import { getTemplateManager } from "../core/template-manager.js";
|
|
6
|
-
const listCommand = new Command()
|
|
7
|
-
.name("list")
|
|
8
|
-
.alias("ls")
|
|
9
|
-
.description("List available remote templates from the repository")
|
|
10
|
-
.option("-p, --provider <provider>", "Filter by provider (stripe, github, etc.)")
|
|
11
|
-
.option("-r, --refresh", "Force refresh the template index cache")
|
|
12
|
-
.action(async (options) => {
|
|
13
|
-
const spinner = ora("Fetching remote templates...").start();
|
|
14
|
-
try {
|
|
15
|
-
const manager = getTemplateManager();
|
|
16
|
-
const templates = await manager.listRemoteTemplates({
|
|
17
|
-
forceRefresh: !!options.refresh,
|
|
18
|
-
});
|
|
19
|
-
spinner.stop();
|
|
20
|
-
if (templates.length === 0) {
|
|
21
|
-
console.log(chalk.yellow("š No remote templates found."));
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
let filtered = templates;
|
|
25
|
-
if (options.provider) {
|
|
26
|
-
filtered = templates.filter((t) => t.metadata.provider.toLowerCase() ===
|
|
27
|
-
options.provider?.toLowerCase());
|
|
28
|
-
}
|
|
29
|
-
if (filtered.length === 0) {
|
|
30
|
-
console.log(chalk.yellow(`š No templates found for provider: ${options.provider}`));
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
console.log(chalk.bold("\nš¦ Available Templates\n"));
|
|
34
|
-
const byProvider = new Map();
|
|
35
|
-
for (const t of filtered) {
|
|
36
|
-
const provider = t.metadata.provider;
|
|
37
|
-
if (!byProvider.has(provider)) {
|
|
38
|
-
byProvider.set(provider, []);
|
|
39
|
-
}
|
|
40
|
-
byProvider.get(provider).push(t);
|
|
41
|
-
}
|
|
42
|
-
for (const [provider, providerTemplates] of byProvider) {
|
|
43
|
-
console.log(chalk.cyan.bold(` ${provider.toUpperCase()}`));
|
|
44
|
-
for (const t of providerTemplates) {
|
|
45
|
-
const status = t.isDownloaded
|
|
46
|
-
? chalk.green("ā downloaded")
|
|
47
|
-
: chalk.gray("ā remote");
|
|
48
|
-
console.log(` ${chalk.white(t.metadata.id)} ${status}`);
|
|
49
|
-
if (t.metadata.description) {
|
|
50
|
-
console.log(chalk.gray(` ${t.metadata.description}`));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
console.log();
|
|
54
|
-
}
|
|
55
|
-
console.log(chalk.gray(` Total: ${filtered.length} templates`));
|
|
56
|
-
console.log(chalk.gray(` Download: better-webhook templates download <id>\n`));
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
spinner.fail("Failed to fetch templates");
|
|
60
|
-
console.error(chalk.red(error.message));
|
|
61
|
-
process.exitCode = 1;
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
const downloadCommand = new Command()
|
|
65
|
-
.name("download")
|
|
66
|
-
.alias("get")
|
|
67
|
-
.argument("[templateId]", "Template ID to download")
|
|
68
|
-
.description("Download a template to local storage")
|
|
69
|
-
.option("-a, --all", "Download all available templates")
|
|
70
|
-
.option("-r, --refresh", "Force refresh the template index cache")
|
|
71
|
-
.action(async (templateId, options) => {
|
|
72
|
-
const manager = getTemplateManager();
|
|
73
|
-
if (options?.all) {
|
|
74
|
-
const spinner = ora("Fetching template list...").start();
|
|
75
|
-
try {
|
|
76
|
-
const templates = await manager.listRemoteTemplates({
|
|
77
|
-
forceRefresh: true,
|
|
78
|
-
});
|
|
79
|
-
const toDownload = templates.filter((t) => !t.isDownloaded);
|
|
80
|
-
spinner.stop();
|
|
81
|
-
if (toDownload.length === 0) {
|
|
82
|
-
console.log(chalk.green("ā All templates already downloaded"));
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
console.log(chalk.bold(`\nDownloading ${toDownload.length} templates...\n`));
|
|
86
|
-
for (const t of toDownload) {
|
|
87
|
-
const downloadSpinner = ora(`Downloading ${t.metadata.id}...`).start();
|
|
88
|
-
try {
|
|
89
|
-
await manager.downloadTemplate(t.metadata.id);
|
|
90
|
-
downloadSpinner.succeed(`Downloaded ${t.metadata.id}`);
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
downloadSpinner.fail(`Failed: ${t.metadata.id} - ${error.message}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
console.log(chalk.green("\nā Download complete\n"));
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
spinner.fail("Failed to fetch templates");
|
|
100
|
-
console.error(chalk.red(error.message));
|
|
101
|
-
process.exitCode = 1;
|
|
102
|
-
}
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (!templateId) {
|
|
106
|
-
const spinner = ora("Fetching templates...").start();
|
|
107
|
-
try {
|
|
108
|
-
const templates = await manager.listRemoteTemplates({
|
|
109
|
-
forceRefresh: !!options?.refresh,
|
|
110
|
-
});
|
|
111
|
-
spinner.stop();
|
|
112
|
-
const notDownloaded = templates.filter((t) => !t.isDownloaded);
|
|
113
|
-
if (notDownloaded.length === 0) {
|
|
114
|
-
console.log(chalk.green("ā All templates already downloaded"));
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const choices = notDownloaded.map((t) => ({
|
|
118
|
-
title: t.metadata.id,
|
|
119
|
-
description: `${t.metadata.provider} - ${t.metadata.event}`,
|
|
120
|
-
value: t.metadata.id,
|
|
121
|
-
}));
|
|
122
|
-
const response = await prompts({
|
|
123
|
-
type: "select",
|
|
124
|
-
name: "templateId",
|
|
125
|
-
message: "Select a template to download:",
|
|
126
|
-
choices,
|
|
127
|
-
});
|
|
128
|
-
if (!response.templateId) {
|
|
129
|
-
console.log(chalk.yellow("Cancelled"));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
templateId = response.templateId;
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
spinner.fail("Failed to fetch templates");
|
|
136
|
-
console.error(chalk.red(error.message));
|
|
137
|
-
process.exitCode = 1;
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
const spinner = ora(`Downloading ${templateId}...`).start();
|
|
142
|
-
try {
|
|
143
|
-
const template = await manager.downloadTemplate(templateId);
|
|
144
|
-
spinner.succeed(`Downloaded ${templateId}`);
|
|
145
|
-
console.log(chalk.gray(` Saved to: ${template.filePath}`));
|
|
146
|
-
console.log(chalk.gray(` Run with: better-webhook run ${templateId}\n`));
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
spinner.fail(`Failed to download ${templateId}`);
|
|
150
|
-
console.error(chalk.red(error.message));
|
|
151
|
-
process.exitCode = 1;
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
const localCommand = new Command()
|
|
155
|
-
.name("local")
|
|
156
|
-
.description("List downloaded local templates")
|
|
157
|
-
.option("-p, --provider <provider>", "Filter by provider")
|
|
158
|
-
.action((options) => {
|
|
159
|
-
const manager = getTemplateManager();
|
|
160
|
-
let templates = manager.listLocalTemplates();
|
|
161
|
-
if (options.provider) {
|
|
162
|
-
templates = templates.filter((t) => t.metadata.provider.toLowerCase() === options.provider?.toLowerCase());
|
|
163
|
-
}
|
|
164
|
-
if (templates.length === 0) {
|
|
165
|
-
console.log(chalk.yellow("\nš No local templates found."));
|
|
166
|
-
console.log(chalk.gray(" Download templates with: better-webhook templates download\n"));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
console.log(chalk.bold("\nš Local Templates\n"));
|
|
170
|
-
const byProvider = new Map();
|
|
171
|
-
for (const t of templates) {
|
|
172
|
-
const provider = t.metadata.provider;
|
|
173
|
-
if (!byProvider.has(provider)) {
|
|
174
|
-
byProvider.set(provider, []);
|
|
175
|
-
}
|
|
176
|
-
byProvider.get(provider).push(t);
|
|
177
|
-
}
|
|
178
|
-
for (const [provider, providerTemplates] of byProvider) {
|
|
179
|
-
console.log(chalk.cyan.bold(` ${provider.toUpperCase()}`));
|
|
180
|
-
for (const t of providerTemplates) {
|
|
181
|
-
console.log(` ${chalk.white(t.id)}`);
|
|
182
|
-
console.log(chalk.gray(` Event: ${t.metadata.event}`));
|
|
183
|
-
console.log(chalk.gray(` Downloaded: ${new Date(t.downloadedAt).toLocaleDateString()}`));
|
|
184
|
-
}
|
|
185
|
-
console.log();
|
|
186
|
-
}
|
|
187
|
-
console.log(chalk.gray(` Total: ${templates.length} templates`));
|
|
188
|
-
console.log(chalk.gray(` Storage: ${manager.getTemplatesDir()}\n`));
|
|
189
|
-
});
|
|
190
|
-
const searchCommand = new Command()
|
|
191
|
-
.name("search")
|
|
192
|
-
.argument("<query>", "Search query")
|
|
193
|
-
.description("Search templates by name, provider, or event")
|
|
194
|
-
.action(async (query) => {
|
|
195
|
-
const spinner = ora("Searching...").start();
|
|
196
|
-
try {
|
|
197
|
-
const manager = getTemplateManager();
|
|
198
|
-
const results = await manager.searchTemplates(query);
|
|
199
|
-
spinner.stop();
|
|
200
|
-
const totalCount = results.remote.length + results.local.length;
|
|
201
|
-
if (totalCount === 0) {
|
|
202
|
-
console.log(chalk.yellow(`\nš No templates found for: "${query}"\n`));
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
console.log(chalk.bold(`\nš Search Results for "${query}"\n`));
|
|
206
|
-
if (results.local.length > 0) {
|
|
207
|
-
console.log(chalk.cyan.bold(" LOCAL TEMPLATES"));
|
|
208
|
-
for (const t of results.local) {
|
|
209
|
-
console.log(` ${chalk.green("ā")} ${t.id} (${t.metadata.provider})`);
|
|
210
|
-
}
|
|
211
|
-
console.log();
|
|
212
|
-
}
|
|
213
|
-
if (results.remote.length > 0) {
|
|
214
|
-
console.log(chalk.cyan.bold(" REMOTE TEMPLATES"));
|
|
215
|
-
for (const t of results.remote) {
|
|
216
|
-
const status = t.isDownloaded ? chalk.green("ā") : chalk.gray("ā");
|
|
217
|
-
console.log(` ${status} ${t.metadata.id} (${t.metadata.provider})`);
|
|
218
|
-
}
|
|
219
|
-
console.log();
|
|
220
|
-
}
|
|
221
|
-
console.log(chalk.gray(` Found: ${totalCount} templates\n`));
|
|
222
|
-
}
|
|
223
|
-
catch (error) {
|
|
224
|
-
spinner.fail("Search failed");
|
|
225
|
-
console.error(chalk.red(error.message));
|
|
226
|
-
process.exitCode = 1;
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
const cacheCommand = new Command()
|
|
230
|
-
.name("cache")
|
|
231
|
-
.description("Manage template cache")
|
|
232
|
-
.option("-c, --clear", "Clear the template cache")
|
|
233
|
-
.action((options) => {
|
|
234
|
-
if (options.clear) {
|
|
235
|
-
const manager = getTemplateManager();
|
|
236
|
-
manager.clearCache();
|
|
237
|
-
console.log(chalk.green("ā Template cache cleared"));
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
console.log("Use --clear to clear the template cache");
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
const cleanCommand = new Command()
|
|
244
|
-
.name("clean")
|
|
245
|
-
.alias("remove-all")
|
|
246
|
-
.description("Remove all downloaded templates")
|
|
247
|
-
.option("-f, --force", "Skip confirmation prompt")
|
|
248
|
-
.action(async (options) => {
|
|
249
|
-
const manager = getTemplateManager();
|
|
250
|
-
const templates = manager.listLocalTemplates();
|
|
251
|
-
if (templates.length === 0) {
|
|
252
|
-
console.log(chalk.yellow("\nš No local templates to remove.\n"));
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
console.log(chalk.bold(`\nšļø Found ${templates.length} downloaded template(s)\n`));
|
|
256
|
-
for (const t of templates) {
|
|
257
|
-
console.log(chalk.gray(` ${t.id} (${t.metadata.provider})`));
|
|
258
|
-
}
|
|
259
|
-
console.log();
|
|
260
|
-
if (!options.force) {
|
|
261
|
-
const response = await prompts({
|
|
262
|
-
type: "confirm",
|
|
263
|
-
name: "confirm",
|
|
264
|
-
message: `Delete all ${templates.length} template(s)?`,
|
|
265
|
-
initial: false,
|
|
266
|
-
});
|
|
267
|
-
if (!response.confirm) {
|
|
268
|
-
console.log(chalk.yellow("Cancelled"));
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
const deleted = manager.deleteAllLocalTemplates();
|
|
273
|
-
console.log(chalk.green(`\nā Removed ${deleted} template(s)`));
|
|
274
|
-
console.log(chalk.gray(` Storage: ${manager.getTemplatesDir()}\n`));
|
|
275
|
-
});
|
|
276
|
-
export const templates = new Command()
|
|
277
|
-
.name("templates")
|
|
278
|
-
.alias("t")
|
|
279
|
-
.description("Manage webhook templates")
|
|
280
|
-
.addCommand(listCommand)
|
|
281
|
-
.addCommand(downloadCommand)
|
|
282
|
-
.addCommand(localCommand)
|
|
283
|
-
.addCommand(searchCommand)
|
|
284
|
-
.addCommand(cacheCommand)
|
|
285
|
-
.addCommand(cleanCommand);
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { CapturedWebhook, CaptureFile } from "../types/index.js";
|
|
2
|
-
export interface CaptureServerOptions {
|
|
3
|
-
capturesDir?: string;
|
|
4
|
-
enableWebSocket?: boolean;
|
|
5
|
-
onCapture?: (args: {
|
|
6
|
-
file: string;
|
|
7
|
-
capture: CapturedWebhook;
|
|
8
|
-
}) => void;
|
|
9
|
-
}
|
|
10
|
-
export declare class CaptureServer {
|
|
11
|
-
private server;
|
|
12
|
-
private wss;
|
|
13
|
-
private capturesDir;
|
|
14
|
-
private clients;
|
|
15
|
-
private captureCount;
|
|
16
|
-
private enableWebSocket;
|
|
17
|
-
private onCapture?;
|
|
18
|
-
constructor(options?: string | CaptureServerOptions);
|
|
19
|
-
getCapturesDir(): string;
|
|
20
|
-
start(port?: number, host?: string): Promise<number>;
|
|
21
|
-
stop(): Promise<void>;
|
|
22
|
-
private handleRequest;
|
|
23
|
-
private detectProvider;
|
|
24
|
-
private broadcast;
|
|
25
|
-
private sendToClient;
|
|
26
|
-
listCaptures(limit?: number): CaptureFile[];
|
|
27
|
-
getCapture(captureId: string): CaptureFile | null;
|
|
28
|
-
deleteCapture(captureId: string): boolean;
|
|
29
|
-
getClientCount(): number;
|
|
30
|
-
}
|
|
31
|
-
export declare function getCaptureServer(capturesDir?: string): CaptureServer;
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import { createServer, } from "http";
|
|
2
|
-
import { WebSocketServer } from "ws";
|
|
3
|
-
import { writeFileSync, mkdirSync, existsSync, readdirSync, readFileSync, unlinkSync, } from "fs";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
import { randomUUID } from "crypto";
|
|
6
|
-
import { homedir } from "os";
|
|
7
|
-
export class CaptureServer {
|
|
8
|
-
server = null;
|
|
9
|
-
wss = null;
|
|
10
|
-
capturesDir;
|
|
11
|
-
clients = new Set();
|
|
12
|
-
captureCount = 0;
|
|
13
|
-
enableWebSocket;
|
|
14
|
-
onCapture;
|
|
15
|
-
constructor(options) {
|
|
16
|
-
const capturesDir = typeof options === "string" ? options : options?.capturesDir;
|
|
17
|
-
this.capturesDir =
|
|
18
|
-
capturesDir || join(homedir(), ".better-webhook", "captures");
|
|
19
|
-
this.enableWebSocket =
|
|
20
|
-
typeof options === "object" ? options?.enableWebSocket !== false : true;
|
|
21
|
-
this.onCapture =
|
|
22
|
-
typeof options === "object" ? options?.onCapture : undefined;
|
|
23
|
-
if (!existsSync(this.capturesDir)) {
|
|
24
|
-
mkdirSync(this.capturesDir, { recursive: true });
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
getCapturesDir() {
|
|
28
|
-
return this.capturesDir;
|
|
29
|
-
}
|
|
30
|
-
async start(port = 3001, host = "0.0.0.0") {
|
|
31
|
-
return new Promise((resolve, reject) => {
|
|
32
|
-
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
33
|
-
if (this.enableWebSocket) {
|
|
34
|
-
this.wss = new WebSocketServer({ server: this.server });
|
|
35
|
-
this.wss.on("connection", (ws) => {
|
|
36
|
-
this.clients.add(ws);
|
|
37
|
-
console.log("š” Dashboard connected via WebSocket");
|
|
38
|
-
ws.on("close", () => {
|
|
39
|
-
this.clients.delete(ws);
|
|
40
|
-
console.log("š” Dashboard disconnected");
|
|
41
|
-
});
|
|
42
|
-
ws.on("error", (error) => {
|
|
43
|
-
console.error("WebSocket error:", error);
|
|
44
|
-
this.clients.delete(ws);
|
|
45
|
-
});
|
|
46
|
-
this.sendToClient(ws, {
|
|
47
|
-
type: "captures_updated",
|
|
48
|
-
payload: {
|
|
49
|
-
captures: this.listCaptures(),
|
|
50
|
-
count: this.captureCount,
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
this.server.on("error", (err) => {
|
|
56
|
-
if (err.code === "EADDRINUSE") {
|
|
57
|
-
reject(new Error(`Port ${port} is already in use`));
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
reject(err);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
this.server.listen(port, host, () => {
|
|
64
|
-
const address = this.server?.address();
|
|
65
|
-
const actualPort = typeof address === "object" ? address?.port || port : port;
|
|
66
|
-
console.log(`\nš£ Webhook Capture Server`);
|
|
67
|
-
console.log(` Listening on http://${host === "0.0.0.0" ? "localhost" : host}:${actualPort}`);
|
|
68
|
-
console.log(` š Captures saved to: ${this.capturesDir}`);
|
|
69
|
-
console.log(` š” Send webhooks to any path to capture them`);
|
|
70
|
-
if (this.enableWebSocket) {
|
|
71
|
-
console.log(` š WebSocket available for real-time updates`);
|
|
72
|
-
}
|
|
73
|
-
console.log(` ā¹ļø Press Ctrl+C to stop\n`);
|
|
74
|
-
resolve(actualPort);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
async stop() {
|
|
79
|
-
return new Promise((resolve) => {
|
|
80
|
-
for (const client of this.clients) {
|
|
81
|
-
client.close();
|
|
82
|
-
}
|
|
83
|
-
this.clients.clear();
|
|
84
|
-
if (this.wss) {
|
|
85
|
-
this.wss.close();
|
|
86
|
-
this.wss = null;
|
|
87
|
-
}
|
|
88
|
-
if (this.server) {
|
|
89
|
-
this.server.close(() => {
|
|
90
|
-
console.log("\nš Capture server stopped");
|
|
91
|
-
resolve();
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
resolve();
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
async handleRequest(req, res) {
|
|
100
|
-
if (req.headers.upgrade?.toLowerCase() === "websocket") {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const timestamp = new Date().toISOString();
|
|
104
|
-
const id = randomUUID();
|
|
105
|
-
const url = req.url || "/";
|
|
106
|
-
const urlParts = new URL(url, `http://${req.headers.host || "localhost"}`);
|
|
107
|
-
const query = {};
|
|
108
|
-
for (const [key, value] of urlParts.searchParams.entries()) {
|
|
109
|
-
if (query[key]) {
|
|
110
|
-
if (Array.isArray(query[key])) {
|
|
111
|
-
query[key].push(value);
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
query[key] = [query[key], value];
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
query[key] = value;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const chunks = [];
|
|
122
|
-
for await (const chunk of req) {
|
|
123
|
-
chunks.push(chunk);
|
|
124
|
-
}
|
|
125
|
-
const rawBody = Buffer.concat(chunks).toString("utf8");
|
|
126
|
-
let body = null;
|
|
127
|
-
const contentType = req.headers["content-type"] || "";
|
|
128
|
-
if (rawBody) {
|
|
129
|
-
if (contentType.includes("application/json")) {
|
|
130
|
-
try {
|
|
131
|
-
body = JSON.parse(rawBody);
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
body = rawBody;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
138
|
-
body = Object.fromEntries(new URLSearchParams(rawBody));
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
body = rawBody;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
const provider = this.detectProvider(req.headers, body);
|
|
145
|
-
const captured = {
|
|
146
|
-
id,
|
|
147
|
-
timestamp,
|
|
148
|
-
method: (req.method || "GET"),
|
|
149
|
-
url,
|
|
150
|
-
path: urlParts.pathname,
|
|
151
|
-
headers: req.headers,
|
|
152
|
-
body,
|
|
153
|
-
rawBody,
|
|
154
|
-
query,
|
|
155
|
-
provider,
|
|
156
|
-
contentType: contentType || undefined,
|
|
157
|
-
contentLength: rawBody.length,
|
|
158
|
-
};
|
|
159
|
-
const date = new Date(timestamp);
|
|
160
|
-
const dateStr = date.toISOString().split("T")[0];
|
|
161
|
-
const timeStr = date
|
|
162
|
-
.toISOString()
|
|
163
|
-
.split("T")[1]
|
|
164
|
-
?.replace(/[:.]/g, "-")
|
|
165
|
-
.slice(0, 8);
|
|
166
|
-
const filename = `${dateStr}_${timeStr}_${id.slice(0, 8)}.json`;
|
|
167
|
-
const filepath = join(this.capturesDir, filename);
|
|
168
|
-
try {
|
|
169
|
-
writeFileSync(filepath, JSON.stringify(captured, null, 2));
|
|
170
|
-
this.captureCount++;
|
|
171
|
-
const providerStr = provider ? ` [${provider}]` : "";
|
|
172
|
-
console.log(`š¦ ${req.method} ${urlParts.pathname}${providerStr} -> ${filename}`);
|
|
173
|
-
this.onCapture?.({ file: filename, capture: captured });
|
|
174
|
-
if (this.enableWebSocket) {
|
|
175
|
-
this.broadcast({
|
|
176
|
-
type: "capture",
|
|
177
|
-
payload: {
|
|
178
|
-
file: filename,
|
|
179
|
-
capture: captured,
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
console.error(`ā Failed to save capture:`, error);
|
|
186
|
-
}
|
|
187
|
-
res.statusCode = 200;
|
|
188
|
-
res.setHeader("Content-Type", "application/json");
|
|
189
|
-
res.setHeader("X-Capture-Id", id);
|
|
190
|
-
res.end(JSON.stringify({
|
|
191
|
-
success: true,
|
|
192
|
-
message: "Webhook captured successfully",
|
|
193
|
-
id,
|
|
194
|
-
timestamp,
|
|
195
|
-
file: filename,
|
|
196
|
-
}));
|
|
197
|
-
}
|
|
198
|
-
detectProvider(headers, body) {
|
|
199
|
-
if (headers["stripe-signature"]) {
|
|
200
|
-
return "stripe";
|
|
201
|
-
}
|
|
202
|
-
if (headers["x-github-event"] || headers["x-hub-signature-256"]) {
|
|
203
|
-
return "github";
|
|
204
|
-
}
|
|
205
|
-
if (headers["x-signature"]) {
|
|
206
|
-
if (body &&
|
|
207
|
-
typeof body === "object" &&
|
|
208
|
-
"type" in body &&
|
|
209
|
-
"payload" in body &&
|
|
210
|
-
"nonce" in body) {
|
|
211
|
-
return "ragie";
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
if (headers["x-shopify-hmac-sha256"] || headers["x-shopify-topic"]) {
|
|
215
|
-
return "shopify";
|
|
216
|
-
}
|
|
217
|
-
if (headers["x-twilio-signature"]) {
|
|
218
|
-
return "twilio";
|
|
219
|
-
}
|
|
220
|
-
if (headers["x-twilio-email-event-webhook-signature"]) {
|
|
221
|
-
return "sendgrid";
|
|
222
|
-
}
|
|
223
|
-
if (headers["x-slack-signature"]) {
|
|
224
|
-
return "slack";
|
|
225
|
-
}
|
|
226
|
-
if (headers["x-signature-ed25519"]) {
|
|
227
|
-
return "discord";
|
|
228
|
-
}
|
|
229
|
-
if (headers["linear-signature"]) {
|
|
230
|
-
return "linear";
|
|
231
|
-
}
|
|
232
|
-
if (headers["svix-signature"]) {
|
|
233
|
-
return "clerk";
|
|
234
|
-
}
|
|
235
|
-
return undefined;
|
|
236
|
-
}
|
|
237
|
-
broadcast(message) {
|
|
238
|
-
const data = JSON.stringify(message);
|
|
239
|
-
for (const client of this.clients) {
|
|
240
|
-
if (client.readyState === 1) {
|
|
241
|
-
client.send(data);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
sendToClient(client, message) {
|
|
246
|
-
if (client.readyState === 1) {
|
|
247
|
-
client.send(JSON.stringify(message));
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
listCaptures(limit = 100) {
|
|
251
|
-
if (!existsSync(this.capturesDir)) {
|
|
252
|
-
return [];
|
|
253
|
-
}
|
|
254
|
-
const files = readdirSync(this.capturesDir)
|
|
255
|
-
.filter((f) => f.endsWith(".json"))
|
|
256
|
-
.sort()
|
|
257
|
-
.reverse()
|
|
258
|
-
.slice(0, limit);
|
|
259
|
-
const captures = [];
|
|
260
|
-
for (const file of files) {
|
|
261
|
-
try {
|
|
262
|
-
const content = readFileSync(join(this.capturesDir, file), "utf-8");
|
|
263
|
-
const capture = JSON.parse(content);
|
|
264
|
-
captures.push({ file, capture });
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return captures;
|
|
270
|
-
}
|
|
271
|
-
getCapture(captureId) {
|
|
272
|
-
const captures = this.listCaptures(1000);
|
|
273
|
-
return (captures.find((c) => c.capture.id === captureId || c.file.includes(captureId)) || null);
|
|
274
|
-
}
|
|
275
|
-
deleteCapture(captureId) {
|
|
276
|
-
const capture = this.getCapture(captureId);
|
|
277
|
-
if (!capture) {
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
280
|
-
try {
|
|
281
|
-
unlinkSync(join(this.capturesDir, capture.file));
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
284
|
-
catch {
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
getClientCount() {
|
|
289
|
-
return this.clients.size;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
let instance = null;
|
|
293
|
-
export function getCaptureServer(capturesDir) {
|
|
294
|
-
if (!instance) {
|
|
295
|
-
instance = new CaptureServer(capturesDir);
|
|
296
|
-
}
|
|
297
|
-
return instance;
|
|
298
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import { type WebSocketMessage } from "../types/index.js";
|
|
3
|
-
export interface DashboardApiOptions {
|
|
4
|
-
capturesDir?: string;
|
|
5
|
-
templatesBaseDir?: string;
|
|
6
|
-
broadcast?: (message: WebSocketMessage) => void;
|
|
7
|
-
}
|
|
8
|
-
export declare function createDashboardApiRouter(options?: DashboardApiOptions): express.Router;
|