hooklens 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,6 +3,93 @@
3
3
  // src/cli/index.ts
4
4
  import { Command as Command7 } from "commander";
5
5
 
6
+ // package.json
7
+ var package_default = {
8
+ name: "hooklens",
9
+ version: "1.1.0",
10
+ description: "Debug webhook signature failures locally.",
11
+ type: "module",
12
+ bin: {
13
+ hooklens: "./dist/index.js"
14
+ },
15
+ scripts: {
16
+ build: "tsup",
17
+ dev: "tsup --watch",
18
+ "docs:build": "vitepress build docs",
19
+ "docs:dev": "vitepress dev docs",
20
+ "docs:preview": "vitepress preview docs",
21
+ start: "node dist/index.js",
22
+ test: "vitest run",
23
+ "test:watch": "vitest",
24
+ "test:coverage": "vitest run --coverage",
25
+ lint: "eslint src/ tests/",
26
+ "lint:fix": "eslint src/ tests/ --fix",
27
+ format: 'prettier --write "src/**/*.ts" "tests/**/*.ts"',
28
+ "format:check": 'prettier --check "src/**/*.ts" "tests/**/*.ts"',
29
+ typecheck: "tsc --noEmit",
30
+ prepublishOnly: "npm run build",
31
+ prepare: "husky"
32
+ },
33
+ keywords: [
34
+ "webhook",
35
+ "debug",
36
+ "stripe",
37
+ "stripe-webhooks",
38
+ "github-webhooks",
39
+ "signature",
40
+ "webhook-signature",
41
+ "replay",
42
+ "cli",
43
+ "developer-tools"
44
+ ],
45
+ author: "Ilia Goginashvili",
46
+ license: "MIT",
47
+ repository: {
48
+ type: "git",
49
+ url: "https://github.com/Ilia01/hooklens.git"
50
+ },
51
+ bugs: {
52
+ url: "https://github.com/Ilia01/hooklens/issues"
53
+ },
54
+ homepage: "https://ilia01.github.io/hooklens/",
55
+ engines: {
56
+ node: ">=24.0.0"
57
+ },
58
+ files: [
59
+ "dist",
60
+ "README.md",
61
+ "LICENSE"
62
+ ],
63
+ devDependencies: {
64
+ "@octokit/webhooks-methods": "^6.0.0",
65
+ "@types/node": "^22.0.0",
66
+ eslint: "^9.0.0",
67
+ husky: "^9.1.7",
68
+ "lint-staged": "^16.4.0",
69
+ prettier: "^3.4.0",
70
+ stripe: "^22.0.0",
71
+ tsup: "^8.0.0",
72
+ typescript: "^5.7.0",
73
+ "typescript-eslint": "^8.0.0",
74
+ vitepress: "^1.6.4",
75
+ vitest: "^3.0.0"
76
+ },
77
+ dependencies: {
78
+ chalk: "^5.4.0",
79
+ commander: "^13.0.0",
80
+ zod: "^4.3.6"
81
+ },
82
+ "lint-staged": {
83
+ "*.{ts,tsx}": [
84
+ "prettier --write",
85
+ "eslint --fix"
86
+ ],
87
+ "*.{json,md,yml,yaml}": [
88
+ "prettier --write"
89
+ ]
90
+ }
91
+ };
92
+
6
93
  // src/errors.ts
7
94
  function toError(value) {
8
95
  return value instanceof Error ? value : new Error(String(value));
@@ -14,6 +101,115 @@ function errorMessage(value) {
14
101
  // src/cli/clear.ts
15
102
  import { Command } from "commander";
16
103
 
104
+ // src/ui/terminal.ts
105
+ import chalk from "chalk";
106
+ function writeLine(stream, line) {
107
+ stream.write(`${line}
108
+ `);
109
+ }
110
+ function formatBodyForDisplay(event) {
111
+ if (event.bodyText !== null) {
112
+ try {
113
+ return JSON.stringify(JSON.parse(event.bodyText), null, 2).split("\n");
114
+ } catch {
115
+ return event.bodyText.split("\n");
116
+ }
117
+ }
118
+ const hex = Buffer.from(event.bodyRaw).toString("hex");
119
+ const lines = hex.match(/.{1,32}/g) ?? [];
120
+ return [`<binary body: ${event.bodyRaw.byteLength} bytes>`, ...lines];
121
+ }
122
+ function verificationLabel(result) {
123
+ if (!result) return chalk.cyan("RECV");
124
+ return result.valid ? chalk.green("PASS") : chalk.red("FAIL");
125
+ }
126
+ function createTerminal(stdout = process.stdout, stderr = process.stderr) {
127
+ return {
128
+ printListenStarted(info) {
129
+ writeLine(
130
+ stdout,
131
+ `${chalk.bold("Listening on")} ${chalk.cyan(`http://127.0.0.1:${info.port}`)}`
132
+ );
133
+ writeLine(stdout, `Verifier: ${info.verifier ?? "none"}`);
134
+ writeLine(stdout, `Forwarding to: ${info.forwardTo ?? "disabled"}`);
135
+ writeLine(stdout, `Storage: ${info.dbPath}`);
136
+ },
137
+ printEventCaptured(event, result) {
138
+ const label = verificationLabel(result);
139
+ const summary = `${label} ${chalk.bold(event.id)} ${event.method} ${event.path}`;
140
+ if (!result) {
141
+ writeLine(stdout, summary);
142
+ return;
143
+ }
144
+ writeLine(stdout, `${summary} ${result.message}`);
145
+ },
146
+ printForwardError(eventId, reason) {
147
+ writeLine(stdout, `${chalk.red("FWD")} ${chalk.bold(eventId)} ${reason}`);
148
+ },
149
+ printForwardRetry(eventId, attempt, maxRetries, reason) {
150
+ writeLine(
151
+ stdout,
152
+ `${chalk.yellow("RETRY")} ${chalk.bold(eventId)} attempt ${attempt}/${maxRetries} ${reason}`
153
+ );
154
+ },
155
+ printEventList(events) {
156
+ if (!events.length) {
157
+ writeLine(stdout, chalk.dim("No stored events."));
158
+ return;
159
+ }
160
+ for (const event of events) {
161
+ const row = `${chalk.dim(event.timestamp)} ${chalk.cyan(event.method)} ${chalk.bold(event.id)} ${event.path}`;
162
+ writeLine(stdout, row);
163
+ }
164
+ },
165
+ printEventDetail(event) {
166
+ writeLine(stdout, `${chalk.bold("Event:")} ${event.id}`);
167
+ writeLine(stdout, `${chalk.bold("Time:")} ${event.timestamp}`);
168
+ writeLine(stdout, `${chalk.bold("Method:")} ${event.method}`);
169
+ writeLine(stdout, `${chalk.bold("Path:")} ${event.path}`);
170
+ if (event.verification) {
171
+ const v = event.verification;
172
+ const label = v.valid ? chalk.green("PASS") : chalk.red("FAIL");
173
+ writeLine(stdout, "");
174
+ writeLine(stdout, chalk.bold("Verification:"));
175
+ writeLine(stdout, ` Result: ${label}`);
176
+ writeLine(stdout, ` Provider: ${v.provider}`);
177
+ writeLine(stdout, ` Message: ${v.message}`);
178
+ }
179
+ writeLine(stdout, "");
180
+ writeLine(stdout, chalk.bold("Headers:"));
181
+ for (const [key, value] of Object.entries(event.headers)) {
182
+ writeLine(stdout, ` ${key}: ${value}`);
183
+ }
184
+ writeLine(stdout, "");
185
+ writeLine(stdout, chalk.bold("Body:"));
186
+ for (const line of formatBodyForDisplay(event)) {
187
+ writeLine(stdout, ` ${line}`);
188
+ }
189
+ },
190
+ printReplayResult(result) {
191
+ const summary = `${chalk.bold("Replay response:")} ${chalk.cyan(String(result.status))}`;
192
+ if (!result.body) {
193
+ writeLine(stdout, summary);
194
+ return;
195
+ }
196
+ writeLine(stdout, `${summary} ${result.body}`);
197
+ },
198
+ printDeleted(eventId) {
199
+ writeLine(stdout, `Deleted ${chalk.bold(eventId)}`);
200
+ },
201
+ printCleared(count) {
202
+ writeLine(stdout, `Cleared ${chalk.bold(String(count))} events`);
203
+ },
204
+ printListenStopped() {
205
+ writeLine(stdout, chalk.dim("Stopped listening."));
206
+ },
207
+ printError(message) {
208
+ writeLine(stderr, chalk.red(message));
209
+ }
210
+ };
211
+ }
212
+
17
213
  // src/storage/index.ts
18
214
  import os from "os";
19
215
  import fs from "fs";
@@ -41,7 +237,9 @@ var webhookEventSchema = z.object({
41
237
  method: z.string(),
42
238
  path: z.string(),
43
239
  headers: z.record(z.string(), z.string()),
44
- body: z.string(),
240
+ bodyRaw: z.custom((value) => value instanceof Uint8Array),
241
+ bodyText: z.string().nullable(),
242
+ bodyExact: z.boolean(),
45
243
  verification: verificationResultSchema.nullable().optional()
46
244
  });
47
245
  var eventRowSchema = z.object({
@@ -50,13 +248,22 @@ var eventRowSchema = z.object({
50
248
  method: z.string(),
51
249
  path: z.string(),
52
250
  headers: z.string(),
53
- body: z.string(),
251
+ body: z.string().nullable().optional(),
252
+ body_raw: z.custom((value) => value === null || value === void 0 || value instanceof Uint8Array).optional(),
54
253
  verification: z.string().nullable().optional()
55
254
  });
56
255
  var replayResultSchema = z.object({
57
256
  status: z.number().int(),
58
257
  body: z.string()
59
258
  });
259
+ function tryDecodeUtf8(raw) {
260
+ if (raw.length === 0) return "";
261
+ const buf = Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength);
262
+ const decoded = buf.toString("utf8");
263
+ const reEncoded = Buffer.from(decoded, "utf8");
264
+ if (buf.length !== reEncoded.length || !buf.equals(reEncoded)) return null;
265
+ return decoded;
266
+ }
60
267
 
61
268
  // src/storage/index.ts
62
269
  var require2 = createRequire(import.meta.url);
@@ -64,15 +271,42 @@ var { DatabaseSync } = require2("node:sqlite");
64
271
  function defaultDbPath() {
65
272
  return path.join(os.homedir(), ".hooklens", "events.db");
66
273
  }
274
+ function eventColumns(db) {
275
+ const rows = db.prepare(`PRAGMA table_info(events)`).all();
276
+ return new Map(rows.map((row) => [row.name, row]));
277
+ }
278
+ function migrateEventsTable(db) {
279
+ const columns = eventColumns(db);
280
+ if (!columns.has("body_raw")) {
281
+ db.exec(`ALTER TABLE events ADD COLUMN body_raw BLOB`);
282
+ }
283
+ if (!columns.has("verification")) {
284
+ db.exec(`ALTER TABLE events ADD COLUMN verification TEXT`);
285
+ }
286
+ if (!columns.has("body")) {
287
+ db.exec(`ALTER TABLE events ADD COLUMN body TEXT`);
288
+ }
289
+ if (columns.has("body_text")) {
290
+ db.exec(`
291
+ UPDATE events
292
+ SET body = COALESCE(body, body_text)
293
+ WHERE body_text IS NOT NULL
294
+ `);
295
+ }
296
+ }
67
297
  function rowToEvent(row) {
68
298
  const verification = row.verification ? verificationResultSchema.parse(JSON.parse(row.verification)) : null;
299
+ const bodyRaw = row.body_raw ?? Buffer.from(row.body ?? "", "utf8");
300
+ const bodyText = row.body_raw ? tryDecodeUtf8(row.body_raw) : row.body ?? "";
69
301
  return webhookEventSchema.parse({
70
302
  id: row.id,
71
303
  timestamp: row.timestamp,
72
304
  method: row.method,
73
305
  path: row.path,
74
306
  headers: JSON.parse(row.headers),
75
- body: row.body,
307
+ bodyRaw,
308
+ bodyText,
309
+ bodyExact: row.body_raw !== null && row.body_raw !== void 0,
76
310
  verification
77
311
  });
78
312
  }
@@ -86,20 +320,15 @@ function createStorage(dbPath) {
86
320
  method TEXT NOT NULL,
87
321
  path TEXT NOT NULL,
88
322
  headers TEXT NOT NULL,
89
- body TEXT NOT NULL,
323
+ body TEXT,
324
+ body_raw BLOB,
90
325
  verification TEXT
91
326
  )
92
327
  `);
93
- try {
94
- db.exec(`ALTER TABLE events ADD COLUMN verification TEXT`);
95
- } catch (error) {
96
- if (!(error instanceof Error && /duplicate column/i.test(error.message))) {
97
- throw error;
98
- }
99
- }
328
+ migrateEventsTable(db);
100
329
  const insertStmt = db.prepare(
101
- `INSERT OR REPLACE INTO events (id, timestamp, method, path, headers, body, verification)
102
- VALUES (?, ?, ?, ?, ?, ?, ?)`
330
+ `INSERT OR REPLACE INTO events (id, timestamp, method, path, headers, body, body_raw, verification)
331
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
103
332
  );
104
333
  const getStmt = db.prepare(`SELECT * FROM events WHERE id = ?`);
105
334
  const listAllStmt = db.prepare(`SELECT * FROM events ORDER BY timestamp DESC`);
@@ -114,7 +343,8 @@ function createStorage(dbPath) {
114
343
  event.method,
115
344
  event.path,
116
345
  JSON.stringify(event.headers),
117
- event.body,
346
+ event.bodyText ?? "",
347
+ event.bodyRaw,
118
348
  event.verification ? JSON.stringify(event.verification) : null
119
349
  );
120
350
  },
@@ -146,107 +376,26 @@ function createStorage(dbPath) {
146
376
  };
147
377
  }
148
378
 
149
- // src/ui/terminal.ts
150
- import chalk from "chalk";
151
- function writeLine(stream, line) {
152
- stream.write(`${line}
153
- `);
154
- }
155
- function verificationLabel(result) {
156
- if (!result) return chalk.cyan("RECV");
157
- return result.valid ? chalk.green("PASS") : chalk.red("FAIL");
158
- }
159
- function createTerminal(stdout = process.stdout, stderr = process.stderr) {
160
- return {
161
- printListenStarted(info) {
162
- writeLine(
163
- stdout,
164
- `${chalk.bold("Listening on")} ${chalk.cyan(`http://127.0.0.1:${info.port}`)}`
165
- );
166
- writeLine(stdout, `Verifier: ${info.verifier ?? "none"}`);
167
- writeLine(stdout, `Forwarding to: ${info.forwardTo ?? "disabled"}`);
168
- writeLine(stdout, `Storage: ${info.dbPath}`);
169
- },
170
- printEventCaptured(event, result) {
171
- const label = verificationLabel(result);
172
- const summary = `${label} ${chalk.bold(event.id)} ${event.method} ${event.path}`;
173
- if (!result) {
174
- writeLine(stdout, summary);
175
- return;
176
- }
177
- writeLine(stdout, `${summary} ${result.message}`);
178
- },
179
- printForwardError(eventId, reason) {
180
- writeLine(stdout, `${chalk.red("FWD")} ${chalk.bold(eventId)} ${reason}`);
181
- },
182
- printForwardRetry(eventId, attempt, maxRetries, reason) {
183
- writeLine(
184
- stdout,
185
- `${chalk.yellow("RETRY")} ${chalk.bold(eventId)} attempt ${attempt}/${maxRetries} ${reason}`
186
- );
187
- },
188
- printEventList(events) {
189
- if (!events.length) {
190
- writeLine(stdout, chalk.dim("No stored events."));
191
- return;
192
- }
193
- for (const event of events) {
194
- const row = `${chalk.dim(event.timestamp)} ${chalk.cyan(event.method)} ${chalk.bold(event.id)} ${event.path}`;
195
- writeLine(stdout, row);
196
- }
197
- },
198
- printEventDetail(event) {
199
- writeLine(stdout, `${chalk.bold("Event:")} ${event.id}`);
200
- writeLine(stdout, `${chalk.bold("Time:")} ${event.timestamp}`);
201
- writeLine(stdout, `${chalk.bold("Method:")} ${event.method}`);
202
- writeLine(stdout, `${chalk.bold("Path:")} ${event.path}`);
203
- if (event.verification) {
204
- const v = event.verification;
205
- const label = v.valid ? chalk.green("PASS") : chalk.red("FAIL");
206
- writeLine(stdout, "");
207
- writeLine(stdout, chalk.bold("Verification:"));
208
- writeLine(stdout, ` Result: ${label}`);
209
- writeLine(stdout, ` Provider: ${v.provider}`);
210
- writeLine(stdout, ` Message: ${v.message}`);
211
- }
212
- writeLine(stdout, "");
213
- writeLine(stdout, chalk.bold("Headers:"));
214
- for (const [key, value] of Object.entries(event.headers)) {
215
- writeLine(stdout, ` ${key}: ${value}`);
216
- }
217
- writeLine(stdout, "");
218
- writeLine(stdout, chalk.bold("Body:"));
219
- let bodyText;
220
- try {
221
- bodyText = JSON.stringify(JSON.parse(event.body), null, 2);
222
- } catch {
223
- bodyText = event.body;
224
- }
225
- for (const line of bodyText.split("\n")) {
226
- writeLine(stdout, ` ${line}`);
227
- }
228
- },
229
- printReplayResult(result) {
230
- const summary = `${chalk.bold("Replay response:")} ${chalk.cyan(String(result.status))}`;
231
- if (!result.body) {
232
- writeLine(stdout, summary);
233
- return;
234
- }
235
- writeLine(stdout, `${summary} ${result.body}`);
236
- },
237
- printDeleted(eventId) {
238
- writeLine(stdout, `Deleted ${chalk.bold(eventId)}`);
239
- },
240
- printCleared(count) {
241
- writeLine(stdout, `Cleared ${chalk.bold(String(count))} events`);
242
- },
243
- printListenStopped() {
244
- writeLine(stdout, chalk.dim("Stopped listening."));
245
- },
246
- printError(message) {
247
- writeLine(stderr, chalk.red(message));
379
+ // src/cli/runtime.ts
380
+ async function withDefaultStorage(run) {
381
+ const storage = createStorage(defaultDbPath());
382
+ try {
383
+ return await run(storage);
384
+ } finally {
385
+ try {
386
+ storage.close();
387
+ } catch {
248
388
  }
249
- };
389
+ }
390
+ }
391
+ async function runCommandAction(run) {
392
+ const terminal = createTerminal();
393
+ try {
394
+ await run(terminal);
395
+ } catch (error) {
396
+ terminal.printError(errorMessage(error));
397
+ process.exitCode = 1;
398
+ }
250
399
  }
251
400
 
252
401
  // src/cli/clear.ts
@@ -269,13 +418,10 @@ async function runClear(flags, deps = {}) {
269
418
  return;
270
419
  }
271
420
  }
272
- const storage = createStorage(defaultDbPath());
273
- try {
421
+ return withDefaultStorage((storage) => {
274
422
  const count = storage.clear();
275
423
  terminal.printCleared(count);
276
- } finally {
277
- storage.close();
278
- }
424
+ });
279
425
  }
280
426
  var clearCommand = new Command("clear").description("Delete all stored webhook events").option("--yes", "Skip confirmation prompt").addHelpText(
281
427
  "after",
@@ -284,29 +430,20 @@ Examples:
284
430
  hooklens clear --yes
285
431
  hooklens clear`
286
432
  ).action(async (options) => {
287
- const terminal = createTerminal();
288
- try {
289
- await runClear(options, { terminal });
290
- } catch (error) {
291
- terminal.printError(errorMessage(error));
292
- process.exitCode = 1;
293
- }
433
+ await runCommandAction((terminal) => runClear(options, { terminal }));
294
434
  });
295
435
 
296
436
  // src/cli/delete.ts
297
437
  import { Command as Command2 } from "commander";
298
438
  async function runDelete(eventId, deps = {}) {
299
439
  const terminal = deps.terminal ?? createTerminal();
300
- const storage = createStorage(defaultDbPath());
301
- try {
440
+ return withDefaultStorage((storage) => {
302
441
  const deleted = storage.delete(eventId);
303
442
  if (!deleted) {
304
443
  throw new Error(`Event "${eventId}" not found.`);
305
444
  }
306
445
  terminal.printDeleted(eventId);
307
- } finally {
308
- storage.close();
309
- }
446
+ });
310
447
  }
311
448
  var deleteCommand = new Command2("delete").description("Delete a stored webhook event").argument("<event-id>", "ID of the event to delete").addHelpText(
312
449
  "after",
@@ -314,24 +451,38 @@ var deleteCommand = new Command2("delete").description("Delete a stored webhook
314
451
  Examples:
315
452
  hooklens delete evt_abc123`
316
453
  ).action(async (eventId) => {
317
- const terminal = createTerminal();
318
- try {
319
- await runDelete(eventId, { terminal });
320
- } catch (error) {
321
- terminal.printError(errorMessage(error));
322
- process.exitCode = 1;
323
- }
454
+ await runCommandAction((terminal) => runDelete(eventId, { terminal }));
324
455
  });
325
456
 
326
457
  // src/cli/inspect.ts
327
458
  import { Command as Command3 } from "commander";
328
459
 
329
460
  // src/cli/json-output.ts
461
+ function toJsonValue(value) {
462
+ if (value instanceof Uint8Array) {
463
+ return Buffer.from(value).toString("base64");
464
+ }
465
+ if (Array.isArray(value)) {
466
+ return value.map(toJsonValue);
467
+ }
468
+ if (value && typeof value === "object") {
469
+ const input = value;
470
+ const output = {};
471
+ for (const [key, entry] of Object.entries(input)) {
472
+ output[key] = toJsonValue(entry);
473
+ }
474
+ if ("bodyRaw" in input && "bodyText" in input && "bodyExact" in input && !("body" in output)) {
475
+ output.body = output.bodyText ?? null;
476
+ }
477
+ return output;
478
+ }
479
+ return value;
480
+ }
330
481
  function writeJsonLine(stdout, data) {
331
- const json = JSON.stringify(data);
482
+ const json = JSON.stringify(toJsonValue(data));
332
483
  if (json === void 0) {
333
484
  throw new Error(
334
- "Cannot serialize value to JSON \u2013 received a non-serializable type (undefined, function, or symbol)"
485
+ "Cannot serialize value to JSON - received a non-serializable type (undefined, function, or symbol)"
335
486
  );
336
487
  }
337
488
  stdout.write(json + "\n");
@@ -340,8 +491,7 @@ function writeJsonLine(stdout, data) {
340
491
  // src/cli/inspect.ts
341
492
  async function runInspect(eventId, flags, deps = {}) {
342
493
  const terminal = deps.terminal ?? createTerminal();
343
- const storage = createStorage(defaultDbPath());
344
- try {
494
+ return withDefaultStorage((storage) => {
345
495
  const event = storage.load(eventId);
346
496
  if (!event) {
347
497
  throw new Error(`Event "${eventId}" not found.`);
@@ -352,9 +502,7 @@ async function runInspect(eventId, flags, deps = {}) {
352
502
  } else {
353
503
  terminal.printEventDetail(event);
354
504
  }
355
- } finally {
356
- storage.close();
357
- }
505
+ });
358
506
  }
359
507
  var inspectCommand = new Command3("inspect").description("View full details of a stored webhook event").argument("<event-id>", "ID of the event to inspect").option("--json", "Output as JSON").addHelpText(
360
508
  "after",
@@ -363,13 +511,7 @@ Examples:
363
511
  hooklens inspect evt_abc123
364
512
  hooklens inspect evt_abc123 --json`
365
513
  ).action(async (eventId, options) => {
366
- const terminal = createTerminal();
367
- try {
368
- await runInspect(eventId, options, { terminal });
369
- } catch (error) {
370
- terminal.printError(errorMessage(error));
371
- process.exitCode = 1;
372
- }
514
+ await runCommandAction((terminal) => runInspect(eventId, options, { terminal }));
373
515
  });
374
516
 
375
517
  // src/cli/listen.ts
@@ -462,7 +604,7 @@ function readBody(req, maxBytes) {
462
604
  }
463
605
  chunks.push(chunk);
464
606
  };
465
- const onEnd = () => resolveOnce(Buffer.concat(chunks, totalBytes).toString("utf8"));
607
+ const onEnd = () => resolveOnce(Buffer.concat(chunks, totalBytes));
466
608
  const onError = (error) => rejectOnce(error);
467
609
  const onSocketClose = () => rejectOnce(new Error("socket closed during request body"));
468
610
  const onSocketError = (error) => rejectOnce(error);
@@ -562,7 +704,7 @@ async function forwardEvent(targetUrl, event, timeoutMs = DEFAULT_FORWARD_TIMEOU
562
704
  const response = await fetch(destination, {
563
705
  method: event.method,
564
706
  headers: headersForForwarding(event.headers),
565
- body: hasBody ? event.body : void 0,
707
+ body: hasBody ? event.bodyRaw : void 0,
566
708
  signal: controller.signal
567
709
  });
568
710
  return {
@@ -593,8 +735,8 @@ function cancellableDelay(ms, req) {
593
735
  timer.unref();
594
736
  });
595
737
  }
596
- function clampRetryCount(value) {
597
- const n = value ?? 0;
738
+ function clampRetryCount(value = 0) {
739
+ const n = value;
598
740
  if (!Number.isFinite(n) || !Number.isInteger(n)) return 0;
599
741
  return Math.max(0, Math.min(n, 10));
600
742
  }
@@ -608,16 +750,19 @@ function createServer(opts) {
608
750
  const retryCount = clampRetryCount(opts.retryCount);
609
751
  const retryBaseDelayMs = opts.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;
610
752
  const handleRequest = async (req, res) => {
611
- const body = await readBody(req, maxBodyBytes);
753
+ const bodyRaw = await readBody(req, maxBodyBytes);
754
+ const bodyText = tryDecodeUtf8(bodyRaw);
612
755
  const event = {
613
756
  id: generateEventId(),
614
757
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
615
758
  method: req.method ?? "GET",
616
759
  path: req.url ?? "/",
617
760
  headers: headersToRecord(req.headers),
618
- body
761
+ bodyRaw,
762
+ bodyText,
763
+ bodyExact: true
619
764
  };
620
- const verification = opts.verifier?.verify({ headers: event.headers, body: event.body }) ?? null;
765
+ const verification = opts.verifier?.verify({ headers: event.headers, bodyRaw }) ?? null;
621
766
  event.verification = verification;
622
767
  opts.storage.save(event);
623
768
  opts.onEvent?.(event, verification);
@@ -736,9 +881,11 @@ function getHeaderCaseInsensitive(headers, name) {
736
881
  return void 0;
737
882
  }
738
883
  function tryCanonicalForm(payload) {
884
+ const text = typeof payload === "string" ? payload : tryDecodeUtf8(payload);
885
+ if (text === null) return null;
739
886
  try {
740
- const canonical = JSON.stringify(JSON.parse(payload));
741
- return canonical === payload ? null : canonical;
887
+ const canonical = JSON.stringify(JSON.parse(text));
888
+ return canonical === text ? null : canonical;
742
889
  } catch {
743
890
  return null;
744
891
  }
@@ -804,7 +951,7 @@ function createGitHubVerifier(opts) {
804
951
  return {
805
952
  provider: PROVIDER,
806
953
  verify: (event) => verifyGitHubSignature({
807
- payload: event.body,
954
+ payload: event.bodyRaw,
808
955
  header: getHeaderCaseInsensitive(event.headers, "x-hub-signature-256"),
809
956
  secret: opts.secret
810
957
  })
@@ -834,8 +981,13 @@ function parseHeader(header) {
834
981
  if (timestamp === null || signatures.length === 0) return null;
835
982
  return { timestamp, signatures };
836
983
  }
837
- function computeHmac2(secret, signedPayload) {
838
- return crypto3.createHmac("sha256", secret).update(signedPayload).digest("hex");
984
+ function computeHmac2(secret, signedPayload2) {
985
+ return crypto3.createHmac("sha256", secret).update(signedPayload2).digest("hex");
986
+ }
987
+ function signedPayload(timestamp, payload) {
988
+ const prefix = Buffer.from(`${timestamp}.`, "utf8");
989
+ const body = typeof payload === "string" ? Buffer.from(payload, "utf8") : Buffer.from(payload);
990
+ return Buffer.concat([prefix, body]);
839
991
  }
840
992
  function constantTimeMatch2(expected, candidates) {
841
993
  const expectedBuf = Buffer.from(expected, "utf8");
@@ -876,14 +1028,13 @@ function verifyStripeSignature(opts) {
876
1028
  `Timestamp is ${minutes} minutes old. Event expired or your clock is drifting.`
877
1029
  );
878
1030
  }
879
- const signedPayload = `${parsed.timestamp}.${opts.payload}`;
880
- const expected = computeHmac2(opts.secret, signedPayload);
1031
+ const expected = computeHmac2(opts.secret, signedPayload(parsed.timestamp, opts.payload));
881
1032
  if (constantTimeMatch2(expected, parsed.signatures)) {
882
1033
  return success2("Signature verified.");
883
1034
  }
884
1035
  const canonical = tryCanonicalForm(opts.payload);
885
1036
  if (canonical !== null) {
886
- const expectedCanonical = computeHmac2(opts.secret, `${parsed.timestamp}.${canonical}`);
1037
+ const expectedCanonical = computeHmac2(opts.secret, signedPayload(parsed.timestamp, canonical));
887
1038
  if (constantTimeMatch2(expectedCanonical, parsed.signatures)) {
888
1039
  return failure2(
889
1040
  "body_mutated",
@@ -900,7 +1051,7 @@ function createStripeVerifier(opts) {
900
1051
  return {
901
1052
  provider: PROVIDER2,
902
1053
  verify: (event) => verifyStripeSignature({
903
- payload: event.body,
1054
+ payload: event.bodyRaw,
904
1055
  header: getHeaderCaseInsensitive(event.headers, "stripe-signature"),
905
1056
  secret: opts.secret,
906
1057
  tolerance: opts.tolerance
@@ -908,6 +1059,12 @@ function createStripeVerifier(opts) {
908
1059
  };
909
1060
  }
910
1061
 
1062
+ // src/cli/defaults.ts
1063
+ var DEFAULT_LISTEN_PORT = 4400;
1064
+ var DEFAULT_LIST_LIMIT = 20;
1065
+ var DEFAULT_REPLAY_TARGET_URL = "http://localhost:3000/webhook";
1066
+ var DEFAULT_RETRY_COUNT = 0;
1067
+
911
1068
  // src/cli/listen.ts
912
1069
  function parsePort(port) {
913
1070
  const raw = port;
@@ -918,7 +1075,7 @@ function parsePort(port) {
918
1075
  return parsed;
919
1076
  }
920
1077
  function parseRetryCount(retry) {
921
- if (retry === void 0) return 0;
1078
+ if (retry === void 0) return DEFAULT_RETRY_COUNT;
922
1079
  const parsed = typeof retry === "number" ? retry : Number(retry);
923
1080
  if (!Number.isInteger(parsed) || parsed < 0 || parsed > 10) {
924
1081
  throw new Error(`Invalid retry count "${retry}". Expected an integer between 0 and 10.`);
@@ -1034,7 +1191,11 @@ async function runListen(flags, deps = {}) {
1034
1191
  throw error;
1035
1192
  }
1036
1193
  }
1037
- var listenCommand = new Command4("listen").description("Start receiving webhooks").option("-p, --port <port>", "Port to listen on", "4400").option("--verify <provider>", "Verify signatures (stripe, github)").option("--secret <secret>", "Webhook signing secret").option("--forward-to <url>", "Forward received webhooks to this URL").option("--retry <count>", "Retry failed forwards with exponential backoff", "0").addHelpText(
1194
+ var listenCommand = new Command4("listen").description("Start receiving webhooks").option("-p, --port <port>", "Port to listen on", String(DEFAULT_LISTEN_PORT)).option("--verify <provider>", "Verify signatures (stripe, github)").option("--secret <secret>", "Webhook signing secret").option("--forward-to <url>", "Forward received webhooks to this URL").option(
1195
+ "--retry <count>",
1196
+ "Retry failed forwards with exponential backoff",
1197
+ String(DEFAULT_RETRY_COUNT)
1198
+ ).addHelpText(
1038
1199
  "after",
1039
1200
  `
1040
1201
  Examples:
@@ -1044,13 +1205,7 @@ Examples:
1044
1205
  hooklens listen --verify github --secret ghsecret_xxx
1045
1206
  hooklens listen --forward-to http://localhost:3000/webhook --retry 3`
1046
1207
  ).action(async (options) => {
1047
- const terminal = createTerminal();
1048
- try {
1049
- await runListen(options, { terminal });
1050
- } catch (error) {
1051
- terminal.printError(errorMessage(error));
1052
- process.exitCode = 1;
1053
- }
1208
+ await runCommandAction((terminal) => runListen(options, { terminal }));
1054
1209
  });
1055
1210
 
1056
1211
  // src/cli/list.ts
@@ -1064,10 +1219,9 @@ function parseLimit(limit) {
1064
1219
  return parsed;
1065
1220
  }
1066
1221
  async function runList(flags, deps = {}) {
1067
- const limit = parseLimit(flags.limit ?? "20");
1222
+ const limit = parseLimit(flags.limit ?? DEFAULT_LIST_LIMIT);
1068
1223
  const terminal = deps.terminal ?? createTerminal();
1069
- const storage = createStorage(defaultDbPath());
1070
- try {
1224
+ return withDefaultStorage((storage) => {
1071
1225
  const events = storage.list(limit);
1072
1226
  if (flags.json) {
1073
1227
  const out = deps.stdout ?? process.stdout;
@@ -1082,11 +1236,9 @@ async function runList(flags, deps = {}) {
1082
1236
  } else {
1083
1237
  terminal.printEventList(events);
1084
1238
  }
1085
- } finally {
1086
- storage.close();
1087
- }
1239
+ });
1088
1240
  }
1089
- var listCommand = new Command5("list").description("Show received webhook events").option("-n, --limit <count>", "Number of events to show", "20").option("--json", "Output as newline-delimited JSON").addHelpText(
1241
+ var listCommand = new Command5("list").description("Show received webhook events").option("-n, --limit <count>", "Number of events to show", String(DEFAULT_LIST_LIMIT)).option("--json", "Output as newline-delimited JSON").addHelpText(
1090
1242
  "after",
1091
1243
  `
1092
1244
  Examples:
@@ -1094,18 +1246,11 @@ Examples:
1094
1246
  hooklens list -n 5
1095
1247
  hooklens list --json`
1096
1248
  ).action(async (options) => {
1097
- const terminal = createTerminal();
1098
- try {
1099
- await runList(options, { terminal });
1100
- } catch (error) {
1101
- terminal.printError(errorMessage(error));
1102
- process.exitCode = 1;
1103
- }
1249
+ await runCommandAction((terminal) => runList(options, { terminal }));
1104
1250
  });
1105
1251
 
1106
1252
  // src/cli/replay.ts
1107
1253
  import { Command as Command6 } from "commander";
1108
- var DEFAULT_REPLAY_TARGET_URL = "http://localhost:3000/webhook";
1109
1254
  function parseTargetUrl(targetUrl) {
1110
1255
  const raw = targetUrl ?? DEFAULT_REPLAY_TARGET_URL;
1111
1256
  try {
@@ -1117,8 +1262,7 @@ function parseTargetUrl(targetUrl) {
1117
1262
  async function runReplay(eventId, flags, deps = {}) {
1118
1263
  const targetUrl = parseTargetUrl(flags.to);
1119
1264
  const terminal = deps.terminal ?? createTerminal();
1120
- const storage = createStorage(defaultDbPath());
1121
- try {
1265
+ return withDefaultStorage(async (storage) => {
1122
1266
  const event = storage.load(eventId);
1123
1267
  if (!event) {
1124
1268
  throw new Error(`Event "${eventId}" not found.`);
@@ -1138,9 +1282,7 @@ async function runReplay(eventId, flags, deps = {}) {
1138
1282
  } catch (error) {
1139
1283
  throw new Error(`Failed to replay "${eventId}" to ${targetUrl}: ${errorMessage(error)}`);
1140
1284
  }
1141
- } finally {
1142
- storage.close();
1143
- }
1285
+ });
1144
1286
  }
1145
1287
  var replayCommand = new Command6("replay").description("Replay a stored webhook event").argument("<event-id>", "ID of the event to replay").option("--to <url>", "Target URL to send the event to", DEFAULT_REPLAY_TARGET_URL).option("--json", "Output as JSON").addHelpText(
1146
1288
  "after",
@@ -1150,18 +1292,12 @@ Examples:
1150
1292
  hooklens replay evt_abc123 --to http://localhost:8080/hook
1151
1293
  hooklens replay evt_abc123 --json`
1152
1294
  ).action(async (eventId, options) => {
1153
- const terminal = createTerminal();
1154
- try {
1155
- await runReplay(eventId, options, { terminal });
1156
- } catch (error) {
1157
- terminal.printError(errorMessage(error));
1158
- process.exitCode = 1;
1159
- }
1295
+ await runCommandAction((terminal) => runReplay(eventId, options, { terminal }));
1160
1296
  });
1161
1297
 
1162
1298
  // src/cli/index.ts
1163
1299
  var program = new Command7();
1164
- program.name("hooklens").description("Inspect, verify, and replay webhooks from your terminal").version("0.1.0");
1300
+ program.name("hooklens").description(package_default.description).version(package_default.version);
1165
1301
  program.addCommand(listenCommand);
1166
1302
  program.addCommand(listCommand);
1167
1303
  program.addCommand(inspectCommand);