hookflare 0.0.1
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/dist/index.js +387 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command6 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/output.ts
|
|
7
|
+
var jsonMode = false;
|
|
8
|
+
function setJsonMode(enabled) {
|
|
9
|
+
jsonMode = enabled;
|
|
10
|
+
}
|
|
11
|
+
function output(data) {
|
|
12
|
+
if (jsonMode) {
|
|
13
|
+
console.log(JSON.stringify(data, null, 2));
|
|
14
|
+
} else {
|
|
15
|
+
console.log(data);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function outputTable(rows) {
|
|
19
|
+
if (jsonMode) {
|
|
20
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (rows.length === 0) {
|
|
24
|
+
console.log("No results.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const keys = Object.keys(rows[0]);
|
|
28
|
+
const widths = keys.map(
|
|
29
|
+
(k) => Math.max(k.length, ...rows.map((r) => String(r[k] ?? "").length))
|
|
30
|
+
);
|
|
31
|
+
const header = keys.map((k, i) => k.padEnd(widths[i])).join(" ");
|
|
32
|
+
const separator = widths.map((w) => "-".repeat(w)).join(" ");
|
|
33
|
+
console.log(header);
|
|
34
|
+
console.log(separator);
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
const line = keys.map((k, i) => String(row[k] ?? "").padEnd(widths[i])).join(" ");
|
|
37
|
+
console.log(line);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function outputSuccess(message) {
|
|
41
|
+
if (jsonMode) {
|
|
42
|
+
console.log(JSON.stringify({ success: true, message }));
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`\u2713 ${message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function outputError(message) {
|
|
48
|
+
if (jsonMode) {
|
|
49
|
+
console.error(JSON.stringify({ success: false, error: message }));
|
|
50
|
+
} else {
|
|
51
|
+
console.error(`\u2717 ${message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/commands/config.ts
|
|
56
|
+
import { Command } from "commander";
|
|
57
|
+
|
|
58
|
+
// src/config.ts
|
|
59
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
60
|
+
import { join } from "path";
|
|
61
|
+
import { homedir } from "os";
|
|
62
|
+
var CONFIG_DIR = join(homedir(), ".hookflare");
|
|
63
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
64
|
+
var DEFAULT_CONFIG = {
|
|
65
|
+
api_url: "http://localhost:8787"
|
|
66
|
+
};
|
|
67
|
+
function loadConfig() {
|
|
68
|
+
if (!existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
69
|
+
try {
|
|
70
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
71
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
72
|
+
} catch {
|
|
73
|
+
return { ...DEFAULT_CONFIG };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function saveConfig(config) {
|
|
77
|
+
const current = loadConfig();
|
|
78
|
+
const merged = { ...current, ...config };
|
|
79
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
80
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
|
|
81
|
+
return merged;
|
|
82
|
+
}
|
|
83
|
+
function getConfigPath() {
|
|
84
|
+
return CONFIG_FILE;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/commands/config.ts
|
|
88
|
+
var configCommand = new Command("config").description("Manage CLI configuration");
|
|
89
|
+
configCommand.command("set").description("Set a configuration value").argument("<key>", "Config key (api_url, token)").argument("<value>", "Config value").action((key, value) => {
|
|
90
|
+
if (!["api_url", "token"].includes(key)) {
|
|
91
|
+
throw new Error(`Unknown config key: ${key}. Valid keys: api_url, token`);
|
|
92
|
+
}
|
|
93
|
+
saveConfig({ [key]: value });
|
|
94
|
+
outputSuccess(`${key} = ${key === "token" ? "****" : value}`);
|
|
95
|
+
});
|
|
96
|
+
configCommand.command("get").description("Show current configuration").action(() => {
|
|
97
|
+
const config = loadConfig();
|
|
98
|
+
const display = {
|
|
99
|
+
...config,
|
|
100
|
+
token: config.token ? "****" : void 0,
|
|
101
|
+
config_path: getConfigPath()
|
|
102
|
+
};
|
|
103
|
+
output(display);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// src/commands/sources.ts
|
|
107
|
+
import { Command as Command2 } from "commander";
|
|
108
|
+
|
|
109
|
+
// src/client.ts
|
|
110
|
+
var HookflareClient = class {
|
|
111
|
+
baseUrl;
|
|
112
|
+
token;
|
|
113
|
+
constructor(opts) {
|
|
114
|
+
const config = loadConfig();
|
|
115
|
+
this.baseUrl = (opts?.apiUrl ?? config.api_url).replace(/\/$/, "");
|
|
116
|
+
this.token = opts?.token ?? config.token;
|
|
117
|
+
}
|
|
118
|
+
headers() {
|
|
119
|
+
const h = { "Content-Type": "application/json" };
|
|
120
|
+
if (this.token) h["Authorization"] = `Bearer ${this.token}`;
|
|
121
|
+
return h;
|
|
122
|
+
}
|
|
123
|
+
async request(method, path, body) {
|
|
124
|
+
const url = `${this.baseUrl}${path}`;
|
|
125
|
+
const res = await fetch(url, {
|
|
126
|
+
method,
|
|
127
|
+
headers: this.headers(),
|
|
128
|
+
body: body ? JSON.stringify(body) : void 0
|
|
129
|
+
});
|
|
130
|
+
const json = await res.json();
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
const msg = json.error?.message ?? `HTTP ${res.status}`;
|
|
133
|
+
throw new Error(msg);
|
|
134
|
+
}
|
|
135
|
+
return json;
|
|
136
|
+
}
|
|
137
|
+
// Sources
|
|
138
|
+
listSources() {
|
|
139
|
+
return this.request("GET", "/api/v1/sources");
|
|
140
|
+
}
|
|
141
|
+
getSource(id) {
|
|
142
|
+
return this.request("GET", `/api/v1/sources/${id}`);
|
|
143
|
+
}
|
|
144
|
+
createSource(body) {
|
|
145
|
+
return this.request("POST", "/api/v1/sources", body);
|
|
146
|
+
}
|
|
147
|
+
updateSource(id, body) {
|
|
148
|
+
return this.request("PUT", `/api/v1/sources/${id}`, body);
|
|
149
|
+
}
|
|
150
|
+
deleteSource(id) {
|
|
151
|
+
return this.request("DELETE", `/api/v1/sources/${id}`);
|
|
152
|
+
}
|
|
153
|
+
// Destinations
|
|
154
|
+
listDestinations() {
|
|
155
|
+
return this.request("GET", "/api/v1/destinations");
|
|
156
|
+
}
|
|
157
|
+
getDestination(id) {
|
|
158
|
+
return this.request("GET", `/api/v1/destinations/${id}`);
|
|
159
|
+
}
|
|
160
|
+
createDestination(body) {
|
|
161
|
+
return this.request("POST", "/api/v1/destinations", body);
|
|
162
|
+
}
|
|
163
|
+
updateDestination(id, body) {
|
|
164
|
+
return this.request("PUT", `/api/v1/destinations/${id}`, body);
|
|
165
|
+
}
|
|
166
|
+
deleteDestination(id) {
|
|
167
|
+
return this.request("DELETE", `/api/v1/destinations/${id}`);
|
|
168
|
+
}
|
|
169
|
+
// Subscriptions
|
|
170
|
+
listSubscriptions() {
|
|
171
|
+
return this.request("GET", "/api/v1/subscriptions");
|
|
172
|
+
}
|
|
173
|
+
createSubscription(body) {
|
|
174
|
+
return this.request("POST", "/api/v1/subscriptions", body);
|
|
175
|
+
}
|
|
176
|
+
deleteSubscription(id) {
|
|
177
|
+
return this.request("DELETE", `/api/v1/subscriptions/${id}`);
|
|
178
|
+
}
|
|
179
|
+
// Events
|
|
180
|
+
listEvents(opts) {
|
|
181
|
+
const params = new URLSearchParams();
|
|
182
|
+
if (opts?.source_id) params.set("source_id", opts.source_id);
|
|
183
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
184
|
+
const qs = params.toString();
|
|
185
|
+
return this.request("GET", `/api/v1/events${qs ? `?${qs}` : ""}`);
|
|
186
|
+
}
|
|
187
|
+
getEvent(id) {
|
|
188
|
+
return this.request("GET", `/api/v1/events/${id}`);
|
|
189
|
+
}
|
|
190
|
+
getEventDeliveries(eventId) {
|
|
191
|
+
return this.request("GET", `/api/v1/events/${eventId}/deliveries`);
|
|
192
|
+
}
|
|
193
|
+
replayEvent(id) {
|
|
194
|
+
return this.request("POST", `/api/v1/events/${id}/replay`);
|
|
195
|
+
}
|
|
196
|
+
// Health
|
|
197
|
+
health() {
|
|
198
|
+
return this.request("GET", "/health");
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/commands/sources.ts
|
|
203
|
+
var sourcesCommand = new Command2("sources").description("Manage webhook sources");
|
|
204
|
+
sourcesCommand.command("list").alias("ls").description("List all sources").action(async () => {
|
|
205
|
+
const client = new HookflareClient();
|
|
206
|
+
const res = await client.listSources();
|
|
207
|
+
const sources = res.data;
|
|
208
|
+
outputTable(
|
|
209
|
+
sources.map((s) => ({
|
|
210
|
+
id: s.id,
|
|
211
|
+
name: s.name,
|
|
212
|
+
verification: s.verification_type ?? "none",
|
|
213
|
+
created_at: s.created_at
|
|
214
|
+
}))
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
sourcesCommand.command("get").description("Get source details").argument("<id>", "Source ID").action(async (id) => {
|
|
218
|
+
const client = new HookflareClient();
|
|
219
|
+
const res = await client.getSource(id);
|
|
220
|
+
output(res.data);
|
|
221
|
+
});
|
|
222
|
+
sourcesCommand.command("create").description("Create a new source").requiredOption("--name <name>", "Source name").option("--verification-type <type>", "Signature verification type (hmac-sha256, hmac-sha1)").option("--verification-secret <secret>", "Shared secret for signature verification").action(async (opts) => {
|
|
223
|
+
const client = new HookflareClient();
|
|
224
|
+
const body = { name: opts.name };
|
|
225
|
+
if (opts.verificationType) {
|
|
226
|
+
body.verification = {
|
|
227
|
+
type: opts.verificationType,
|
|
228
|
+
secret: opts.verificationSecret
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const res = await client.createSource(body);
|
|
232
|
+
output(res.data);
|
|
233
|
+
outputSuccess("Source created");
|
|
234
|
+
});
|
|
235
|
+
sourcesCommand.command("delete").alias("rm").description("Delete a source").argument("<id>", "Source ID").action(async (id) => {
|
|
236
|
+
const client = new HookflareClient();
|
|
237
|
+
await client.deleteSource(id);
|
|
238
|
+
outputSuccess(`Source ${id} deleted`);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// src/commands/destinations.ts
|
|
242
|
+
import { Command as Command3 } from "commander";
|
|
243
|
+
var destinationsCommand = new Command3("destinations").alias("dest").description("Manage webhook destinations");
|
|
244
|
+
destinationsCommand.command("list").alias("ls").description("List all destinations").action(async () => {
|
|
245
|
+
const client = new HookflareClient();
|
|
246
|
+
const res = await client.listDestinations();
|
|
247
|
+
const dests = res.data;
|
|
248
|
+
outputTable(
|
|
249
|
+
dests.map((d) => ({
|
|
250
|
+
id: d.id,
|
|
251
|
+
name: d.name,
|
|
252
|
+
url: d.url,
|
|
253
|
+
max_retries: d.max_retries,
|
|
254
|
+
created_at: d.created_at
|
|
255
|
+
}))
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
destinationsCommand.command("get").description("Get destination details").argument("<id>", "Destination ID").action(async (id) => {
|
|
259
|
+
const client = new HookflareClient();
|
|
260
|
+
const res = await client.getDestination(id);
|
|
261
|
+
output(res.data);
|
|
262
|
+
});
|
|
263
|
+
destinationsCommand.command("create").description("Create a new destination").requiredOption("--name <name>", "Destination name").requiredOption("--url <url>", "Target URL").option("--max-retries <n>", "Maximum retry attempts", "5").option("--timeout-ms <n>", "Request timeout in ms", "30000").action(async (opts) => {
|
|
264
|
+
const client = new HookflareClient();
|
|
265
|
+
const res = await client.createDestination({
|
|
266
|
+
name: opts.name,
|
|
267
|
+
url: opts.url,
|
|
268
|
+
retry_policy: {
|
|
269
|
+
max_retries: parseInt(opts.maxRetries, 10),
|
|
270
|
+
timeout_ms: parseInt(opts.timeoutMs, 10)
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
output(res.data);
|
|
274
|
+
outputSuccess("Destination created");
|
|
275
|
+
});
|
|
276
|
+
destinationsCommand.command("delete").alias("rm").description("Delete a destination").argument("<id>", "Destination ID").action(async (id) => {
|
|
277
|
+
const client = new HookflareClient();
|
|
278
|
+
await client.deleteDestination(id);
|
|
279
|
+
outputSuccess(`Destination ${id} deleted`);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// src/commands/subscriptions.ts
|
|
283
|
+
import { Command as Command4 } from "commander";
|
|
284
|
+
var subscriptionsCommand = new Command4("subscriptions").alias("subs").description("Manage webhook subscriptions (source \u2192 destination routing)");
|
|
285
|
+
subscriptionsCommand.command("list").alias("ls").description("List all subscriptions").action(async () => {
|
|
286
|
+
const client = new HookflareClient();
|
|
287
|
+
const res = await client.listSubscriptions();
|
|
288
|
+
const subs = res.data;
|
|
289
|
+
outputTable(
|
|
290
|
+
subs.map((s) => ({
|
|
291
|
+
id: s.id,
|
|
292
|
+
source_id: s.source_id,
|
|
293
|
+
destination_id: s.destination_id,
|
|
294
|
+
event_types: s.event_types,
|
|
295
|
+
enabled: s.enabled
|
|
296
|
+
}))
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
subscriptionsCommand.command("create").description("Create a new subscription").requiredOption("--source <id>", "Source ID").requiredOption("--destination <id>", "Destination ID").option("--events <types...>", "Event type filters (default: *)").action(async (opts) => {
|
|
300
|
+
const client = new HookflareClient();
|
|
301
|
+
const res = await client.createSubscription({
|
|
302
|
+
source_id: opts.source,
|
|
303
|
+
destination_id: opts.destination,
|
|
304
|
+
event_types: opts.events ?? ["*"]
|
|
305
|
+
});
|
|
306
|
+
output(res.data);
|
|
307
|
+
outputSuccess("Subscription created");
|
|
308
|
+
});
|
|
309
|
+
subscriptionsCommand.command("delete").alias("rm").description("Delete a subscription").argument("<id>", "Subscription ID").action(async (id) => {
|
|
310
|
+
const client = new HookflareClient();
|
|
311
|
+
await client.deleteSubscription(id);
|
|
312
|
+
outputSuccess(`Subscription ${id} deleted`);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// src/commands/events.ts
|
|
316
|
+
import { Command as Command5 } from "commander";
|
|
317
|
+
var eventsCommand = new Command5("events").description("View webhook events and deliveries");
|
|
318
|
+
eventsCommand.command("list").alias("ls").description("List received events").option("--source <id>", "Filter by source ID").option("--limit <n>", "Max results", "20").action(async (opts) => {
|
|
319
|
+
const client = new HookflareClient();
|
|
320
|
+
const res = await client.listEvents({
|
|
321
|
+
source_id: opts.source,
|
|
322
|
+
limit: parseInt(opts.limit, 10)
|
|
323
|
+
});
|
|
324
|
+
const events = res.data;
|
|
325
|
+
outputTable(
|
|
326
|
+
events.map((e) => ({
|
|
327
|
+
id: e.id,
|
|
328
|
+
source_id: e.source_id,
|
|
329
|
+
event_type: e.event_type ?? "-",
|
|
330
|
+
received_at: e.received_at
|
|
331
|
+
}))
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
eventsCommand.command("get").description("Get event details with payload").argument("<id>", "Event ID").action(async (id) => {
|
|
335
|
+
const client = new HookflareClient();
|
|
336
|
+
const res = await client.getEvent(id);
|
|
337
|
+
output(res.data);
|
|
338
|
+
});
|
|
339
|
+
eventsCommand.command("deliveries").description("List delivery attempts for an event").argument("<event_id>", "Event ID").action(async (eventId) => {
|
|
340
|
+
const client = new HookflareClient();
|
|
341
|
+
const res = await client.getEventDeliveries(eventId);
|
|
342
|
+
const deliveries = res.data;
|
|
343
|
+
outputTable(
|
|
344
|
+
deliveries.map((d) => ({
|
|
345
|
+
id: d.id,
|
|
346
|
+
destination_id: d.destination_id,
|
|
347
|
+
status: d.status,
|
|
348
|
+
attempt: d.attempt,
|
|
349
|
+
status_code: d.status_code ?? "-",
|
|
350
|
+
latency_ms: d.latency_ms ?? "-"
|
|
351
|
+
}))
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
eventsCommand.command("replay").description("Replay an event to its destinations").argument("<id>", "Event ID").action(async (id) => {
|
|
355
|
+
const client = new HookflareClient();
|
|
356
|
+
await client.replayEvent(id);
|
|
357
|
+
outputSuccess(`Event ${id} replayed`);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// src/index.ts
|
|
361
|
+
var program = new Command6();
|
|
362
|
+
program.name("hookflare").description("CLI for hookflare \u2014 open-source webhook infrastructure").version("0.0.1").option("--json", "Output in JSON format (agent-friendly)").hook("preAction", (thisCommand) => {
|
|
363
|
+
const opts = thisCommand.optsWithGlobals();
|
|
364
|
+
if (opts.json) setJsonMode(true);
|
|
365
|
+
});
|
|
366
|
+
program.command("health").description("Check connection to hookflare server").action(async () => {
|
|
367
|
+
const client = new HookflareClient();
|
|
368
|
+
const res = await client.health();
|
|
369
|
+
console.log(JSON.stringify(res, null, 2));
|
|
370
|
+
});
|
|
371
|
+
program.addCommand(configCommand);
|
|
372
|
+
program.addCommand(sourcesCommand);
|
|
373
|
+
program.addCommand(destinationsCommand);
|
|
374
|
+
program.addCommand(subscriptionsCommand);
|
|
375
|
+
program.addCommand(eventsCommand);
|
|
376
|
+
program.exitOverride();
|
|
377
|
+
async function main() {
|
|
378
|
+
try {
|
|
379
|
+
await program.parseAsync(process.argv);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
if (err instanceof Error && err.message !== "(outputHelp)") {
|
|
382
|
+
outputError(err.message);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hookflare",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI for hookflare — open-source webhook infrastructure",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hookflare": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"commander": "^13.1.0",
|
|
14
|
+
"@hookflare/shared": "0.1.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.0.0",
|
|
18
|
+
"tsup": "^8.4.0",
|
|
19
|
+
"tsx": "^4.19.0",
|
|
20
|
+
"typescript": "^5.8.0"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"webhook",
|
|
24
|
+
"hookflare",
|
|
25
|
+
"cloudflare",
|
|
26
|
+
"cli"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/hookedge/hookflare",
|
|
32
|
+
"directory": "packages/cli"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/hookedge/hookflare",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsx src/index.ts",
|
|
38
|
+
"typecheck": "tsc --noEmit"
|
|
39
|
+
}
|
|
40
|
+
}
|