hooklens 0.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/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +782 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilia Goginashvili
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# HookLens
|
|
4
|
+
|
|
5
|
+
**Inspect, verify, and replay webhooks from your terminal.**
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://github.com/Ilia01/hooklens/actions/workflows/ci.yml)
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
> [!WARNING]
|
|
13
|
+
> HookLens is under active development and not yet published to npm. Star or watch the repo to get notified when the first release drops.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
Every developer who's integrated Stripe, Paddle, or any webhook provider has hit the same wall: signature verification fails, the error says nothing useful, and you spend an hour staring at raw headers trying to figure out what went wrong.
|
|
18
|
+
|
|
19
|
+
HookLens sits between the webhook provider and your local server. It captures the raw request, verifies the signature, and tells you _why_ it failed -- not just "invalid signature" but the actual reason. Then it stores the event so you can replay it whenever you want.
|
|
20
|
+
|
|
21
|
+
## How it works
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
hooklens listen --verify stripe --secret whsec_xxx --forward-to http://localhost:3000/webhook
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
When a webhook arrives, HookLens:
|
|
28
|
+
|
|
29
|
+
1. Captures the raw body before any framework parses it
|
|
30
|
+
2. Verifies the provider signature and prints PASS or FAIL with a reason
|
|
31
|
+
3. Stores the event locally for replay
|
|
32
|
+
4. Forwards it to your app
|
|
33
|
+
|
|
34
|
+
Something break? Check what came in and replay it:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
hooklens list
|
|
38
|
+
hooklens replay evt_abc123 --to http://localhost:3000/webhook
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> [!IMPORTANT]
|
|
42
|
+
> HookLens is **not** a tunnel. You still need [ngrok](https://ngrok.com), [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/), or a similar tool to expose your local server to the internet. HookLens handles what happens after the request arrives.
|
|
43
|
+
|
|
44
|
+
## Why signature verification breaks
|
|
45
|
+
|
|
46
|
+
This is the problem HookLens exists to solve.
|
|
47
|
+
|
|
48
|
+
Webhook providers (Stripe, Paddle, GitHub, etc.) sign every request using the raw body. Your server is supposed to recompute that signature and compare. Simple in theory, but:
|
|
49
|
+
|
|
50
|
+
- **Express** parses the body into JSON before your route handler sees it. When you `JSON.stringify()` it back, key ordering or whitespace changes. Different string = different hash = verification fails.
|
|
51
|
+
- **Next.js** and **Fastify** do the same thing in different ways.
|
|
52
|
+
- The error you get? `"Webhook signature verification failed."` -- thanks for nothing.
|
|
53
|
+
|
|
54
|
+
HookLens intercepts the request at the HTTP level using `node:http` directly, before any framework touches the body. It verifies against the actual bytes that arrived over the wire, and when it fails, it tells you which of the 5 possible failure modes you hit:
|
|
55
|
+
|
|
56
|
+
| Failure | What HookLens tells you |
|
|
57
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| Missing header | `stripe-signature header not found. Is this actually from Stripe?` |
|
|
59
|
+
| Wrong secret | `Signature mismatch. Check your webhook secret matches the Stripe dashboard.` |
|
|
60
|
+
| Expired timestamp | `Timestamp is 47 minutes old. Event expired or your clock is drifting.` |
|
|
61
|
+
| Body mutated | `Signature mismatch with correct secret. Body was likely parsed and re-serialized by your framework.` |
|
|
62
|
+
| Malformed header | `stripe-signature header is malformed. Expected format: t=timestamp,v1=signature` |
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
> [!NOTE]
|
|
67
|
+
> Not on npm yet. For now, you can clone and build locally.
|
|
68
|
+
|
|
69
|
+
**Requires Node.js 24 or newer** (HookLens uses the built-in `node:sqlite` module).
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/Ilia01/hooklens.git
|
|
73
|
+
cd hooklens
|
|
74
|
+
npm install
|
|
75
|
+
npm run build
|
|
76
|
+
npm link
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Commands
|
|
80
|
+
|
|
81
|
+
| Command | Description |
|
|
82
|
+
| ---------------------- | ------------------------ |
|
|
83
|
+
| `hooklens listen` | Start receiving webhooks |
|
|
84
|
+
| `hooklens list` | Show stored events |
|
|
85
|
+
| `hooklens replay <id>` | Resend a stored event |
|
|
86
|
+
|
|
87
|
+
### `hooklens listen`
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
hooklens listen --port 4400 --verify stripe --secret whsec_xxx --forward-to http://localhost:3000/webhook
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| Flag | Default | Description |
|
|
94
|
+
| --------------------- | ------- | ------------------------------------- |
|
|
95
|
+
| `-p, --port <port>` | `4400` | Port to listen on |
|
|
96
|
+
| `--verify <provider>` | -- | Verify signatures (`stripe`) |
|
|
97
|
+
| `--secret <secret>` | -- | Webhook signing secret |
|
|
98
|
+
| `--forward-to <url>` | -- | Forward received webhooks to this URL |
|
|
99
|
+
|
|
100
|
+
### `hooklens list`
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
hooklens list --limit 10
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| Flag | Default | Description |
|
|
107
|
+
| --------------------- | ------- | ------------------------ |
|
|
108
|
+
| `-n, --limit <count>` | `20` | Number of events to show |
|
|
109
|
+
|
|
110
|
+
### `hooklens replay`
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
hooklens replay evt_abc123 --to http://localhost:3000/webhook
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Flag | Default | Description |
|
|
117
|
+
| ------------ | ------------------------------- | ------------------------------- |
|
|
118
|
+
| `--to <url>` | `http://localhost:3000/webhook` | Target URL to send the event to |
|
|
119
|
+
|
|
120
|
+
## Supported providers
|
|
121
|
+
|
|
122
|
+
- **Stripe** -- full signature verification with detailed failure messages
|
|
123
|
+
|
|
124
|
+
More providers will be added based on demand. [Open an issue](https://github.com/Ilia01/hooklens/issues) to request one.
|
|
125
|
+
|
|
126
|
+
## Contributing
|
|
127
|
+
|
|
128
|
+
Want to add a provider, fix a bug, or improve something? Here's how to get set up:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
git clone https://github.com/Ilia01/hooklens.git
|
|
132
|
+
cd hooklens
|
|
133
|
+
npm install
|
|
134
|
+
npm run dev
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`npm run dev` watches for changes and rebuilds automatically. You can test your changes by running `hooklens` commands directly against the local build.
|
|
138
|
+
|
|
139
|
+
Please open an issue before starting work on anything significant so we can discuss the approach.
|
|
140
|
+
|
|
141
|
+
When you're ready to submit, make sure CI will be happy:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm test
|
|
145
|
+
npm run typecheck
|
|
146
|
+
npm run lint
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
[MIT](LICENSE)
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command as Command4 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/cli/listen.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/server/index.ts
|
|
10
|
+
import http from "http";
|
|
11
|
+
import crypto from "crypto";
|
|
12
|
+
var FORWARD_STRIP = /* @__PURE__ */ new Set([
|
|
13
|
+
"connection",
|
|
14
|
+
"keep-alive",
|
|
15
|
+
"proxy-authenticate",
|
|
16
|
+
"proxy-authorization",
|
|
17
|
+
"te",
|
|
18
|
+
"trailer",
|
|
19
|
+
"transfer-encoding",
|
|
20
|
+
"upgrade",
|
|
21
|
+
"host",
|
|
22
|
+
"content-length"
|
|
23
|
+
]);
|
|
24
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
25
|
+
var DEFAULT_FORWARD_TIMEOUT_MS = 5e3;
|
|
26
|
+
function forwardedStripSet(headers) {
|
|
27
|
+
const strip = new Set(FORWARD_STRIP);
|
|
28
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
29
|
+
if (key.toLowerCase() !== "connection") continue;
|
|
30
|
+
for (const token of value.split(/[,\s]+/)) {
|
|
31
|
+
const name = token.trim().toLowerCase();
|
|
32
|
+
if (name) strip.add(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return strip;
|
|
36
|
+
}
|
|
37
|
+
function generateEventId() {
|
|
38
|
+
return `evt_${crypto.randomBytes(12).toString("base64url")}`;
|
|
39
|
+
}
|
|
40
|
+
var PayloadTooLargeError = class extends Error {
|
|
41
|
+
constructor(maxBytes) {
|
|
42
|
+
super(`payload too large: max ${maxBytes} bytes`);
|
|
43
|
+
this.maxBytes = maxBytes;
|
|
44
|
+
this.name = "PayloadTooLargeError";
|
|
45
|
+
}
|
|
46
|
+
maxBytes;
|
|
47
|
+
};
|
|
48
|
+
function isPayloadTooLargeError(error) {
|
|
49
|
+
return error instanceof PayloadTooLargeError;
|
|
50
|
+
}
|
|
51
|
+
function requestSockets(req) {
|
|
52
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
53
|
+
sockets.add(req.socket);
|
|
54
|
+
const proxiedSocket = req.socket.proxy;
|
|
55
|
+
if (proxiedSocket) sockets.add(proxiedSocket);
|
|
56
|
+
return [...sockets];
|
|
57
|
+
}
|
|
58
|
+
function readBody(req, maxBytes) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let totalBytes = 0;
|
|
62
|
+
let settled = false;
|
|
63
|
+
const sockets = requestSockets(req);
|
|
64
|
+
const cleanup = () => {
|
|
65
|
+
req.off("data", onData);
|
|
66
|
+
req.off("end", onEnd);
|
|
67
|
+
req.off("error", onError);
|
|
68
|
+
for (const socket of sockets) {
|
|
69
|
+
socket.off("close", onSocketClose);
|
|
70
|
+
socket.off("error", onSocketError);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const rejectOnce = (error) => {
|
|
74
|
+
if (settled) return;
|
|
75
|
+
settled = true;
|
|
76
|
+
cleanup();
|
|
77
|
+
reject(error);
|
|
78
|
+
};
|
|
79
|
+
const resolveOnce = (body) => {
|
|
80
|
+
if (settled) return;
|
|
81
|
+
settled = true;
|
|
82
|
+
cleanup();
|
|
83
|
+
resolve(body);
|
|
84
|
+
};
|
|
85
|
+
const onData = (chunk) => {
|
|
86
|
+
totalBytes += chunk.length;
|
|
87
|
+
if (totalBytes > maxBytes) {
|
|
88
|
+
req.resume();
|
|
89
|
+
rejectOnce(new PayloadTooLargeError(maxBytes));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
chunks.push(chunk);
|
|
93
|
+
};
|
|
94
|
+
const onEnd = () => resolveOnce(Buffer.concat(chunks, totalBytes).toString("utf8"));
|
|
95
|
+
const onError = (error) => rejectOnce(error);
|
|
96
|
+
const onSocketClose = () => rejectOnce(new Error("socket closed during request body"));
|
|
97
|
+
const onSocketError = (error) => rejectOnce(error);
|
|
98
|
+
req.on("data", onData);
|
|
99
|
+
req.on("end", onEnd);
|
|
100
|
+
req.on("error", onError);
|
|
101
|
+
for (const socket of sockets) {
|
|
102
|
+
socket.on("close", onSocketClose);
|
|
103
|
+
socket.on("error", onSocketError);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function headersToRecord(headers) {
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
110
|
+
if (value === void 0) continue;
|
|
111
|
+
out[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function headersForForwarding(headers) {
|
|
116
|
+
const out = {};
|
|
117
|
+
const strip = forwardedStripSet(headers);
|
|
118
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
119
|
+
if (!strip.has(key.toLowerCase())) out[key] = value;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function forwardPathname(targetPathname, incomingPathname) {
|
|
124
|
+
if (targetPathname === "/" || targetPathname === "") return incomingPathname || "/";
|
|
125
|
+
if (incomingPathname === "/" || incomingPathname === "") return targetPathname;
|
|
126
|
+
const base = targetPathname.endsWith("/") ? targetPathname.slice(0, -1) : targetPathname;
|
|
127
|
+
const incoming = incomingPathname.startsWith("/") ? incomingPathname : `/${incomingPathname}`;
|
|
128
|
+
return `${base}${incoming}`;
|
|
129
|
+
}
|
|
130
|
+
function parseEventPath(path2) {
|
|
131
|
+
if (/^[A-Za-z][A-Za-z\d+.-]*:/.test(path2)) {
|
|
132
|
+
const parsed = new URL(path2);
|
|
133
|
+
return { pathname: parsed.pathname, search: parsed.search };
|
|
134
|
+
}
|
|
135
|
+
const queryIndex = path2.indexOf("?");
|
|
136
|
+
if (queryIndex === -1) {
|
|
137
|
+
return { pathname: path2, search: "" };
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
pathname: path2.slice(0, queryIndex),
|
|
141
|
+
search: path2.slice(queryIndex)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function mergeForwardSearch(targetSearch, incomingSearch) {
|
|
145
|
+
const merged = new URLSearchParams(incomingSearch);
|
|
146
|
+
const trusted = new URLSearchParams(targetSearch);
|
|
147
|
+
for (const key of new Set(trusted.keys())) {
|
|
148
|
+
merged.delete(key);
|
|
149
|
+
}
|
|
150
|
+
for (const [key, value] of trusted) {
|
|
151
|
+
merged.append(key, value);
|
|
152
|
+
}
|
|
153
|
+
const search = merged.toString();
|
|
154
|
+
return search.length > 0 ? `?${search}` : "";
|
|
155
|
+
}
|
|
156
|
+
function isAbortError(error) {
|
|
157
|
+
return error instanceof Error && error.name === "AbortError";
|
|
158
|
+
}
|
|
159
|
+
async function forwardEvent(targetUrl, event, timeoutMs = DEFAULT_FORWARD_TIMEOUT_MS) {
|
|
160
|
+
const target = new URL(targetUrl);
|
|
161
|
+
const destination = new URL(target.href);
|
|
162
|
+
const parsedEventPath = parseEventPath(event.path);
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
165
|
+
destination.pathname = forwardPathname(destination.pathname, parsedEventPath.pathname);
|
|
166
|
+
destination.search = mergeForwardSearch(destination.search, parsedEventPath.search);
|
|
167
|
+
try {
|
|
168
|
+
const hasBody = event.method !== "GET" && event.method !== "HEAD";
|
|
169
|
+
const response = await fetch(destination, {
|
|
170
|
+
method: event.method,
|
|
171
|
+
headers: headersForForwarding(event.headers),
|
|
172
|
+
body: hasBody ? event.body : void 0,
|
|
173
|
+
signal: controller.signal
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
status: response.status,
|
|
177
|
+
body: await response.text()
|
|
178
|
+
};
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (isAbortError(error)) {
|
|
181
|
+
throw new Error(`forward timed out after ${timeoutMs}ms`);
|
|
182
|
+
}
|
|
183
|
+
throw error;
|
|
184
|
+
} finally {
|
|
185
|
+
clearTimeout(timeout);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function createServer(opts) {
|
|
189
|
+
let boundPort = opts.port;
|
|
190
|
+
let httpServer = null;
|
|
191
|
+
let isStarting = false;
|
|
192
|
+
const maxBodyBytes = opts.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
193
|
+
const forwardTimeoutMs = opts.forwardTimeoutMs ?? DEFAULT_FORWARD_TIMEOUT_MS;
|
|
194
|
+
const handleRequest = async (req, res) => {
|
|
195
|
+
const body = await readBody(req, maxBodyBytes);
|
|
196
|
+
const event = {
|
|
197
|
+
id: generateEventId(),
|
|
198
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
199
|
+
method: req.method ?? "GET",
|
|
200
|
+
path: req.url ?? "/",
|
|
201
|
+
headers: headersToRecord(req.headers),
|
|
202
|
+
body
|
|
203
|
+
};
|
|
204
|
+
opts.storage.save(event);
|
|
205
|
+
const verification = opts.verifier?.verify({ headers: event.headers, body: event.body }) ?? null;
|
|
206
|
+
opts.onEvent?.(event, verification);
|
|
207
|
+
if (!opts.forwardTo) {
|
|
208
|
+
res.statusCode = 200;
|
|
209
|
+
res.end("ok");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const forwarded = await forwardEvent(opts.forwardTo, event, forwardTimeoutMs);
|
|
214
|
+
res.statusCode = forwarded.status;
|
|
215
|
+
res.end(forwarded.body);
|
|
216
|
+
} catch {
|
|
217
|
+
res.statusCode = 502;
|
|
218
|
+
res.end("bad gateway");
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
get port() {
|
|
223
|
+
return boundPort;
|
|
224
|
+
},
|
|
225
|
+
async start() {
|
|
226
|
+
if (httpServer || isStarting) {
|
|
227
|
+
throw new Error("server already started");
|
|
228
|
+
}
|
|
229
|
+
isStarting = true;
|
|
230
|
+
const server = http.createServer((req, res) => {
|
|
231
|
+
handleRequest(req, res).catch((err) => {
|
|
232
|
+
if (isPayloadTooLargeError(err)) {
|
|
233
|
+
res.statusCode = 413;
|
|
234
|
+
res.end(err.message);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
res.statusCode = 500;
|
|
238
|
+
res.end(err instanceof Error ? err.message : String(err));
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
httpServer = server;
|
|
242
|
+
try {
|
|
243
|
+
await new Promise((resolve, reject) => {
|
|
244
|
+
const onError = (err) => {
|
|
245
|
+
server.off("error", onError);
|
|
246
|
+
if (httpServer === server) httpServer = null;
|
|
247
|
+
boundPort = opts.port;
|
|
248
|
+
isStarting = false;
|
|
249
|
+
reject(err);
|
|
250
|
+
};
|
|
251
|
+
server.once("error", onError);
|
|
252
|
+
server.listen(opts.port, "127.0.0.1", () => {
|
|
253
|
+
server.off("error", onError);
|
|
254
|
+
const addr = server.address();
|
|
255
|
+
if (addr && typeof addr !== "string") {
|
|
256
|
+
boundPort = addr.port;
|
|
257
|
+
}
|
|
258
|
+
isStarting = false;
|
|
259
|
+
resolve();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
} catch (err) {
|
|
263
|
+
if (httpServer === server) httpServer = null;
|
|
264
|
+
boundPort = opts.port;
|
|
265
|
+
isStarting = false;
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
async stop() {
|
|
270
|
+
if (!httpServer) return;
|
|
271
|
+
const server = httpServer;
|
|
272
|
+
await new Promise((resolve, reject) => {
|
|
273
|
+
server.close((err) => {
|
|
274
|
+
if (httpServer === server) httpServer = null;
|
|
275
|
+
boundPort = opts.port;
|
|
276
|
+
isStarting = false;
|
|
277
|
+
if (err) {
|
|
278
|
+
reject(err);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
resolve();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/storage/index.ts
|
|
289
|
+
import os from "os";
|
|
290
|
+
import fs from "fs";
|
|
291
|
+
import { createRequire } from "module";
|
|
292
|
+
import path from "path";
|
|
293
|
+
|
|
294
|
+
// src/types.ts
|
|
295
|
+
import { z } from "zod";
|
|
296
|
+
var webhookEventSchema = z.object({
|
|
297
|
+
id: z.string(),
|
|
298
|
+
timestamp: z.string(),
|
|
299
|
+
method: z.string(),
|
|
300
|
+
path: z.string(),
|
|
301
|
+
headers: z.record(z.string(), z.string()),
|
|
302
|
+
body: z.string()
|
|
303
|
+
});
|
|
304
|
+
var eventRowSchema = z.object({
|
|
305
|
+
id: z.string(),
|
|
306
|
+
timestamp: z.string(),
|
|
307
|
+
method: z.string(),
|
|
308
|
+
path: z.string(),
|
|
309
|
+
headers: z.string(),
|
|
310
|
+
body: z.string()
|
|
311
|
+
});
|
|
312
|
+
var verificationResultSchema = z.object({
|
|
313
|
+
valid: z.boolean(),
|
|
314
|
+
provider: z.string(),
|
|
315
|
+
message: z.string(),
|
|
316
|
+
code: z.enum([
|
|
317
|
+
"valid",
|
|
318
|
+
"missing_header",
|
|
319
|
+
"malformed_header",
|
|
320
|
+
"expired_timestamp",
|
|
321
|
+
"signature_mismatch",
|
|
322
|
+
"body_mutated"
|
|
323
|
+
])
|
|
324
|
+
});
|
|
325
|
+
var replayResultSchema = z.object({
|
|
326
|
+
status: z.number().int(),
|
|
327
|
+
body: z.string()
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// src/storage/index.ts
|
|
331
|
+
var require2 = createRequire(import.meta.url);
|
|
332
|
+
var { DatabaseSync } = require2("node:sqlite");
|
|
333
|
+
function defaultDbPath() {
|
|
334
|
+
return path.join(os.homedir(), ".hooklens", "events.db");
|
|
335
|
+
}
|
|
336
|
+
function rowToEvent(row) {
|
|
337
|
+
return webhookEventSchema.parse({
|
|
338
|
+
id: row.id,
|
|
339
|
+
timestamp: row.timestamp,
|
|
340
|
+
method: row.method,
|
|
341
|
+
path: row.path,
|
|
342
|
+
headers: JSON.parse(row.headers),
|
|
343
|
+
body: row.body
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function createStorage(dbPath) {
|
|
347
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
348
|
+
const db = new DatabaseSync(dbPath);
|
|
349
|
+
db.exec(`
|
|
350
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
351
|
+
id TEXT PRIMARY KEY,
|
|
352
|
+
timestamp TEXT NOT NULL,
|
|
353
|
+
method TEXT NOT NULL,
|
|
354
|
+
path TEXT NOT NULL,
|
|
355
|
+
headers TEXT NOT NULL,
|
|
356
|
+
body TEXT NOT NULL
|
|
357
|
+
)
|
|
358
|
+
`);
|
|
359
|
+
const insertStmt = db.prepare(
|
|
360
|
+
`INSERT OR REPLACE INTO events (id, timestamp, method, path, headers, body)
|
|
361
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
362
|
+
);
|
|
363
|
+
const getStmt = db.prepare(`SELECT * FROM events WHERE id = ?`);
|
|
364
|
+
const listAllStmt = db.prepare(`SELECT * FROM events ORDER BY timestamp DESC`);
|
|
365
|
+
const listLimitedStmt = db.prepare(`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`);
|
|
366
|
+
const clearStmt = db.prepare(`DELETE FROM events`);
|
|
367
|
+
return {
|
|
368
|
+
save(event) {
|
|
369
|
+
insertStmt.run(
|
|
370
|
+
event.id,
|
|
371
|
+
event.timestamp,
|
|
372
|
+
event.method,
|
|
373
|
+
event.path,
|
|
374
|
+
JSON.stringify(event.headers),
|
|
375
|
+
event.body
|
|
376
|
+
);
|
|
377
|
+
},
|
|
378
|
+
load(id) {
|
|
379
|
+
const raw = getStmt.get(id);
|
|
380
|
+
if (!raw) return null;
|
|
381
|
+
const row = eventRowSchema.parse(raw);
|
|
382
|
+
return rowToEvent(row);
|
|
383
|
+
},
|
|
384
|
+
list(limit) {
|
|
385
|
+
if (limit !== void 0 && (!Number.isInteger(limit) || limit <= 0)) {
|
|
386
|
+
throw new Error(`Invalid limit: must be a positive integer, got ${limit}`);
|
|
387
|
+
}
|
|
388
|
+
const raw = limit === void 0 ? listAllStmt.all() : listLimitedStmt.all(limit);
|
|
389
|
+
const rows = raw.map((r) => eventRowSchema.parse(r));
|
|
390
|
+
return rows.map(rowToEvent);
|
|
391
|
+
},
|
|
392
|
+
clear() {
|
|
393
|
+
clearStmt.run();
|
|
394
|
+
},
|
|
395
|
+
close() {
|
|
396
|
+
db.close();
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/ui/terminal.ts
|
|
402
|
+
import chalk from "chalk";
|
|
403
|
+
function writeLine(stream, line) {
|
|
404
|
+
stream.write(`${line}
|
|
405
|
+
`);
|
|
406
|
+
}
|
|
407
|
+
function verificationLabel(result) {
|
|
408
|
+
if (!result) return chalk.cyan("RECV");
|
|
409
|
+
return result.valid ? chalk.green("PASS") : chalk.red("FAIL");
|
|
410
|
+
}
|
|
411
|
+
function createTerminal(stdout = process.stdout, stderr = process.stderr) {
|
|
412
|
+
return {
|
|
413
|
+
printListenStarted(info) {
|
|
414
|
+
writeLine(
|
|
415
|
+
stdout,
|
|
416
|
+
`${chalk.bold("Listening on")} ${chalk.cyan(`http://127.0.0.1:${info.port}`)}`
|
|
417
|
+
);
|
|
418
|
+
writeLine(stdout, `Verifier: ${info.verifier ?? "none"}`);
|
|
419
|
+
writeLine(stdout, `Forwarding to: ${info.forwardTo ?? "disabled"}`);
|
|
420
|
+
writeLine(stdout, `Storage: ${info.dbPath}`);
|
|
421
|
+
},
|
|
422
|
+
printEventCaptured(event, result) {
|
|
423
|
+
const label = verificationLabel(result);
|
|
424
|
+
const summary = `${label} ${chalk.bold(event.id)} ${event.method} ${event.path}`;
|
|
425
|
+
if (!result) {
|
|
426
|
+
writeLine(stdout, summary);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
writeLine(stdout, `${summary} ${result.message}`);
|
|
430
|
+
},
|
|
431
|
+
printEventList(events) {
|
|
432
|
+
if (!events.length) {
|
|
433
|
+
writeLine(stdout, chalk.dim("No stored events."));
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
for (const event of events) {
|
|
437
|
+
const row = `${chalk.dim(event.timestamp)} ${chalk.cyan(event.method)} ${chalk.bold(event.id)} ${event.path}`;
|
|
438
|
+
writeLine(stdout, row);
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
printReplayResult(result) {
|
|
442
|
+
const summary = `${chalk.bold("Replay response:")} ${chalk.cyan(String(result.status))}`;
|
|
443
|
+
if (!result.body) {
|
|
444
|
+
writeLine(stdout, summary);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
writeLine(stdout, `${summary} ${result.body}`);
|
|
448
|
+
},
|
|
449
|
+
printListenStopped() {
|
|
450
|
+
writeLine(stdout, chalk.dim("Stopped listening."));
|
|
451
|
+
},
|
|
452
|
+
printError(message) {
|
|
453
|
+
writeLine(stderr, chalk.red(message));
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/verify/stripe.ts
|
|
459
|
+
import crypto2 from "crypto";
|
|
460
|
+
|
|
461
|
+
// src/verify/headers.ts
|
|
462
|
+
function getHeaderCaseInsensitive(headers, name) {
|
|
463
|
+
const expected = name.toLowerCase();
|
|
464
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
465
|
+
if (key.toLowerCase() === expected) return value;
|
|
466
|
+
}
|
|
467
|
+
return void 0;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/verify/stripe.ts
|
|
471
|
+
var DEFAULT_TOLERANCE_SECONDS = 300;
|
|
472
|
+
var PROVIDER = "stripe";
|
|
473
|
+
function parseHeader(header) {
|
|
474
|
+
let timestamp = null;
|
|
475
|
+
const signatures = [];
|
|
476
|
+
for (const part of header.split(",")) {
|
|
477
|
+
const eqIdx = part.indexOf("=");
|
|
478
|
+
if (eqIdx === -1) return null;
|
|
479
|
+
const key = part.slice(0, eqIdx);
|
|
480
|
+
const value = part.slice(eqIdx + 1);
|
|
481
|
+
if (key === "t") {
|
|
482
|
+
if (!/^\d+$/.test(value)) return null;
|
|
483
|
+
timestamp = Number(value);
|
|
484
|
+
} else if (key === "v1") {
|
|
485
|
+
if (value.length === 0) return null;
|
|
486
|
+
signatures.push(value);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (timestamp === null || signatures.length === 0) return null;
|
|
490
|
+
return { timestamp, signatures };
|
|
491
|
+
}
|
|
492
|
+
function computeHmac(secret, signedPayload) {
|
|
493
|
+
return crypto2.createHmac("sha256", secret).update(signedPayload).digest("hex");
|
|
494
|
+
}
|
|
495
|
+
function constantTimeMatch(expected, candidates) {
|
|
496
|
+
const expectedBuf = Buffer.from(expected, "utf8");
|
|
497
|
+
for (const candidate of candidates) {
|
|
498
|
+
if (candidate.length !== expected.length) continue;
|
|
499
|
+
const candidateBuf = Buffer.from(candidate, "utf8");
|
|
500
|
+
if (crypto2.timingSafeEqual(expectedBuf, candidateBuf)) return true;
|
|
501
|
+
}
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
function tryCanonicalForm(payload) {
|
|
505
|
+
try {
|
|
506
|
+
const canonical = JSON.stringify(JSON.parse(payload));
|
|
507
|
+
return canonical === payload ? null : canonical;
|
|
508
|
+
} catch {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function success(message) {
|
|
513
|
+
return { valid: true, provider: PROVIDER, code: "valid", message };
|
|
514
|
+
}
|
|
515
|
+
function failure(code, message) {
|
|
516
|
+
return { valid: false, provider: PROVIDER, code, message };
|
|
517
|
+
}
|
|
518
|
+
function verifyStripeSignature(opts) {
|
|
519
|
+
if (!opts.header) {
|
|
520
|
+
return failure(
|
|
521
|
+
"missing_header",
|
|
522
|
+
"stripe-signature header not found. Is this actually from Stripe?"
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const parsed = parseHeader(opts.header);
|
|
526
|
+
if (!parsed) {
|
|
527
|
+
return failure(
|
|
528
|
+
"malformed_header",
|
|
529
|
+
"stripe-signature header is malformed. Expected format: t=timestamp,v1=signature"
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
const tolerance = opts.tolerance ?? DEFAULT_TOLERANCE_SECONDS;
|
|
533
|
+
const nowMs = (opts.now ?? Date.now)();
|
|
534
|
+
const ageSeconds = Math.floor(nowMs / 1e3) - parsed.timestamp;
|
|
535
|
+
if (ageSeconds > tolerance) {
|
|
536
|
+
const minutes = Math.floor(ageSeconds / 60);
|
|
537
|
+
return failure(
|
|
538
|
+
"expired_timestamp",
|
|
539
|
+
`Timestamp is ${minutes} minutes old. Event expired or your clock is drifting.`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const signedPayload = `${parsed.timestamp}.${opts.payload}`;
|
|
543
|
+
const expected = computeHmac(opts.secret, signedPayload);
|
|
544
|
+
if (constantTimeMatch(expected, parsed.signatures)) {
|
|
545
|
+
return success("Signature verified.");
|
|
546
|
+
}
|
|
547
|
+
const canonical = tryCanonicalForm(opts.payload);
|
|
548
|
+
if (canonical !== null) {
|
|
549
|
+
const expectedCanonical = computeHmac(opts.secret, `${parsed.timestamp}.${canonical}`);
|
|
550
|
+
if (constantTimeMatch(expectedCanonical, parsed.signatures)) {
|
|
551
|
+
return failure(
|
|
552
|
+
"body_mutated",
|
|
553
|
+
"Signature mismatch with correct secret. Body was likely parsed and re-serialized by your framework."
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return failure(
|
|
558
|
+
"signature_mismatch",
|
|
559
|
+
"Signature mismatch. Check your webhook secret matches the Stripe dashboard."
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
function createStripeVerifier(opts) {
|
|
563
|
+
return {
|
|
564
|
+
provider: PROVIDER,
|
|
565
|
+
verify: (event) => verifyStripeSignature({
|
|
566
|
+
payload: event.body,
|
|
567
|
+
header: getHeaderCaseInsensitive(event.headers, "stripe-signature"),
|
|
568
|
+
secret: opts.secret,
|
|
569
|
+
tolerance: opts.tolerance
|
|
570
|
+
})
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/cli/listen.ts
|
|
575
|
+
function parsePort(port) {
|
|
576
|
+
const raw = port;
|
|
577
|
+
const parsed = typeof raw === "number" ? raw : Number(raw);
|
|
578
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
579
|
+
throw new Error(`Invalid port "${raw}". Expected an integer between 0 and 65535.`);
|
|
580
|
+
}
|
|
581
|
+
return parsed;
|
|
582
|
+
}
|
|
583
|
+
function buildVerifier(flags) {
|
|
584
|
+
if (!flags.verify) return void 0;
|
|
585
|
+
switch (flags.verify) {
|
|
586
|
+
case "stripe": {
|
|
587
|
+
if (!flags.secret) {
|
|
588
|
+
throw new Error("--secret is required when --verify stripe is set");
|
|
589
|
+
}
|
|
590
|
+
return createStripeVerifier({ secret: flags.secret });
|
|
591
|
+
}
|
|
592
|
+
default:
|
|
593
|
+
throw new Error(`Unknown --verify provider "${flags.verify}". Supported: stripe`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async function stopServer(server) {
|
|
597
|
+
if (!server) return;
|
|
598
|
+
await server.stop();
|
|
599
|
+
}
|
|
600
|
+
function printEventCapturedBestEffort(terminal, event, result) {
|
|
601
|
+
try {
|
|
602
|
+
terminal.printEventCaptured(event, result);
|
|
603
|
+
} catch (error) {
|
|
604
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
605
|
+
console.error(`Failed to print captured event: ${message}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async function runListen(flags, deps = {}) {
|
|
609
|
+
const port = parsePort(flags.port);
|
|
610
|
+
const verifier = buildVerifier(flags);
|
|
611
|
+
const dbPath = defaultDbPath();
|
|
612
|
+
const signals = deps.signals ?? process;
|
|
613
|
+
const terminal = deps.terminal ?? createTerminal();
|
|
614
|
+
const storage = createStorage(dbPath);
|
|
615
|
+
let server = null;
|
|
616
|
+
let cleanedUp = false;
|
|
617
|
+
let listenersAttached = false;
|
|
618
|
+
const cleanup = async (printStopped) => {
|
|
619
|
+
if (cleanedUp) return;
|
|
620
|
+
cleanedUp = true;
|
|
621
|
+
if (listenersAttached) {
|
|
622
|
+
signals.off("SIGINT", onSignal);
|
|
623
|
+
signals.off("SIGTERM", onSignal);
|
|
624
|
+
listenersAttached = false;
|
|
625
|
+
}
|
|
626
|
+
let stopError = null;
|
|
627
|
+
try {
|
|
628
|
+
await stopServer(server);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
stopError = error;
|
|
631
|
+
} finally {
|
|
632
|
+
storage.close();
|
|
633
|
+
}
|
|
634
|
+
if (stopError) {
|
|
635
|
+
throw stopError;
|
|
636
|
+
}
|
|
637
|
+
if (printStopped) {
|
|
638
|
+
terminal.printListenStopped();
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
let settle = null;
|
|
642
|
+
let fail = null;
|
|
643
|
+
const shutdown = new Promise((resolve, reject) => {
|
|
644
|
+
settle = resolve;
|
|
645
|
+
fail = reject;
|
|
646
|
+
});
|
|
647
|
+
const onSignal = () => {
|
|
648
|
+
void cleanup(true).then(
|
|
649
|
+
() => settle?.(),
|
|
650
|
+
(error) => fail?.(error)
|
|
651
|
+
);
|
|
652
|
+
};
|
|
653
|
+
try {
|
|
654
|
+
server = createServer({
|
|
655
|
+
port,
|
|
656
|
+
storage,
|
|
657
|
+
verifier,
|
|
658
|
+
forwardTo: flags.forwardTo,
|
|
659
|
+
onEvent: (event, result) => printEventCapturedBestEffort(terminal, event, result)
|
|
660
|
+
});
|
|
661
|
+
signals.on("SIGINT", onSignal);
|
|
662
|
+
signals.on("SIGTERM", onSignal);
|
|
663
|
+
listenersAttached = true;
|
|
664
|
+
await server.start();
|
|
665
|
+
if (cleanedUp) {
|
|
666
|
+
return await shutdown;
|
|
667
|
+
}
|
|
668
|
+
terminal.printListenStarted({
|
|
669
|
+
port: server.port,
|
|
670
|
+
dbPath,
|
|
671
|
+
verifier: verifier?.provider,
|
|
672
|
+
forwardTo: flags.forwardTo
|
|
673
|
+
});
|
|
674
|
+
return await shutdown;
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (cleanedUp) {
|
|
677
|
+
return await shutdown;
|
|
678
|
+
}
|
|
679
|
+
await cleanup(false);
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
var listenCommand = new Command("listen").description("Start receiving webhooks").option("-p, --port <port>", "Port to listen on", "4400").option("--verify <provider>", "Verify signatures (stripe)").option("--secret <secret>", "Webhook signing secret").option("--forward-to <url>", "Forward received webhooks to this URL").action(async (options) => {
|
|
684
|
+
const terminal = createTerminal();
|
|
685
|
+
try {
|
|
686
|
+
await runListen(options, { terminal });
|
|
687
|
+
} catch (error) {
|
|
688
|
+
terminal.printError(error instanceof Error ? error.message : String(error));
|
|
689
|
+
process.exitCode = 1;
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// src/cli/list.ts
|
|
694
|
+
import { Command as Command2 } from "commander";
|
|
695
|
+
function parseLimit(limit) {
|
|
696
|
+
const raw = limit;
|
|
697
|
+
const parsed = typeof raw === "number" ? raw : Number(raw);
|
|
698
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
699
|
+
throw new Error(`Invalid limit "${raw}". Expected a positive integer.`);
|
|
700
|
+
}
|
|
701
|
+
return parsed;
|
|
702
|
+
}
|
|
703
|
+
async function runList(flags, deps = {}) {
|
|
704
|
+
const limit = parseLimit(flags.limit ?? "20");
|
|
705
|
+
const terminal = deps.terminal ?? createTerminal();
|
|
706
|
+
const storage = createStorage(defaultDbPath());
|
|
707
|
+
try {
|
|
708
|
+
const events = storage.list(limit);
|
|
709
|
+
terminal.printEventList(events);
|
|
710
|
+
} finally {
|
|
711
|
+
storage.close();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
var listCommand = new Command2("list").description("Show received webhook events").option("-n, --limit <count>", "Number of events to show", "20").action(async (options) => {
|
|
715
|
+
const terminal = createTerminal();
|
|
716
|
+
try {
|
|
717
|
+
await runList(options, { terminal });
|
|
718
|
+
} catch (error) {
|
|
719
|
+
terminal.printError(error instanceof Error ? error.message : String(error));
|
|
720
|
+
process.exitCode = 1;
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// src/cli/replay.ts
|
|
725
|
+
import { Command as Command3 } from "commander";
|
|
726
|
+
var DEFAULT_REPLAY_TARGET_URL = "http://localhost:3000/webhook";
|
|
727
|
+
function parseTargetUrl(targetUrl) {
|
|
728
|
+
const raw = targetUrl ?? DEFAULT_REPLAY_TARGET_URL;
|
|
729
|
+
try {
|
|
730
|
+
return new URL(raw).href;
|
|
731
|
+
} catch {
|
|
732
|
+
throw new Error(`Invalid target URL "${raw}".`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
async function runReplay(eventId, flags, deps = {}) {
|
|
736
|
+
const targetUrl = parseTargetUrl(flags.to);
|
|
737
|
+
const terminal = deps.terminal ?? createTerminal();
|
|
738
|
+
const storage = createStorage(defaultDbPath());
|
|
739
|
+
try {
|
|
740
|
+
const event = storage.load(eventId);
|
|
741
|
+
if (!event) {
|
|
742
|
+
throw new Error(`Event "${eventId}" not found.`);
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
const result = await forwardEvent(targetUrl, event);
|
|
746
|
+
const body = result.body.length <= 200 ? result.body : `${result.body.slice(0, 197)}...`;
|
|
747
|
+
terminal.printReplayResult({
|
|
748
|
+
status: result.status,
|
|
749
|
+
body
|
|
750
|
+
});
|
|
751
|
+
} catch (error) {
|
|
752
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
753
|
+
throw new Error(`Failed to replay "${eventId}" to ${targetUrl}: ${message}`);
|
|
754
|
+
}
|
|
755
|
+
} finally {
|
|
756
|
+
storage.close();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
var replayCommand = new Command3("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).action(async (eventId, options) => {
|
|
760
|
+
const terminal = createTerminal();
|
|
761
|
+
try {
|
|
762
|
+
await runReplay(eventId, options, { terminal });
|
|
763
|
+
} catch (error) {
|
|
764
|
+
terminal.printError(error instanceof Error ? error.message : String(error));
|
|
765
|
+
process.exitCode = 1;
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// src/cli/index.ts
|
|
770
|
+
var program = new Command4();
|
|
771
|
+
program.name("hooklens").description("Inspect, verify, and replay webhooks from your terminal").version("0.1.0");
|
|
772
|
+
program.addCommand(listenCommand);
|
|
773
|
+
program.addCommand(listCommand);
|
|
774
|
+
program.addCommand(replayCommand);
|
|
775
|
+
try {
|
|
776
|
+
await program.parseAsync(process.argv);
|
|
777
|
+
} catch (error) {
|
|
778
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
779
|
+
console.error(message);
|
|
780
|
+
process.exitCode = 1;
|
|
781
|
+
}
|
|
782
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/index.ts","../src/cli/listen.ts","../src/server/index.ts","../src/storage/index.ts","../src/types.ts","../src/ui/terminal.ts","../src/verify/stripe.ts","../src/verify/headers.ts","../src/cli/list.ts","../src/cli/replay.ts"],"sourcesContent":["import { Command } from 'commander'\nimport { listenCommand } from './listen.js'\nimport { listCommand } from './list.js'\nimport { replayCommand } from './replay.js'\n\nconst program = new Command()\n\nprogram\n .name('hooklens')\n .description('Inspect, verify, and replay webhooks from your terminal')\n .version('0.1.0')\n\nprogram.addCommand(listenCommand)\nprogram.addCommand(listCommand)\nprogram.addCommand(replayCommand)\n\ntry {\n await program.parseAsync(process.argv)\n} catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n console.error(message)\n process.exitCode = 1\n}\n","import { Command } from 'commander'\nimport { createServer, type Server } from '../server/index.js'\nimport { createStorage, defaultDbPath } from '../storage/index.js'\nimport type { VerificationResult, Verifier, WebhookEvent } from '../types.js'\nimport { createTerminal, type TerminalUI } from '../ui/terminal.js'\nimport { createStripeVerifier } from '../verify/stripe.js'\n\nexport interface ListenFlags {\n port?: string | number\n verify?: string\n secret?: string\n forwardTo?: string\n}\n\nexport interface SignalBus {\n on(event: 'SIGINT' | 'SIGTERM', listener: () => void): void\n off(event: 'SIGINT' | 'SIGTERM', listener: () => void): void\n}\n\nexport interface ListenDeps {\n signals?: SignalBus\n terminal?: TerminalUI\n}\n\nfunction parsePort(port: string | number | undefined): number {\n const raw = port\n const parsed = typeof raw === 'number' ? raw : Number(raw)\n\n if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65_535) {\n throw new Error(`Invalid port \"${raw}\". Expected an integer between 0 and 65535.`)\n }\n\n return parsed\n}\n\n/** Maps --verify flags to a Verifier. See CONTRIBUTING.md → Adding a provider. */\nexport function buildVerifier(flags: ListenFlags): Verifier | undefined {\n if (!flags.verify) return undefined\n\n switch (flags.verify) {\n case 'stripe': {\n if (!flags.secret) {\n throw new Error('--secret is required when --verify stripe is set')\n }\n return createStripeVerifier({ secret: flags.secret })\n }\n default:\n throw new Error(`Unknown --verify provider \"${flags.verify}\". Supported: stripe`)\n }\n}\n\nasync function stopServer(server: Server | null): Promise<void> {\n if (!server) return\n await server.stop()\n}\n\nfunction printEventCapturedBestEffort(\n terminal: TerminalUI,\n event: WebhookEvent,\n result: VerificationResult | null,\n): void {\n try {\n terminal.printEventCaptured(event, result)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n console.error(`Failed to print captured event: ${message}`)\n }\n}\n\nexport async function runListen(flags: ListenFlags, deps: ListenDeps = {}): Promise<void> {\n const port = parsePort(flags.port)\n const verifier = buildVerifier(flags)\n const dbPath = defaultDbPath()\n const signals = deps.signals ?? process\n const terminal = deps.terminal ?? createTerminal()\n const storage = createStorage(dbPath)\n\n let server: Server | null = null\n let cleanedUp = false\n let listenersAttached = false\n\n const cleanup = async (printStopped: boolean): Promise<void> => {\n if (cleanedUp) return\n cleanedUp = true\n if (listenersAttached) {\n signals.off('SIGINT', onSignal)\n signals.off('SIGTERM', onSignal)\n listenersAttached = false\n }\n\n let stopError: unknown = null\n\n try {\n await stopServer(server)\n } catch (error) {\n stopError = error\n } finally {\n storage.close()\n }\n\n if (stopError) {\n throw stopError\n }\n\n if (printStopped) {\n terminal.printListenStopped()\n }\n }\n\n let settle: (() => void) | null = null\n let fail: ((error: unknown) => void) | null = null\n const shutdown = new Promise<void>((resolve, reject) => {\n settle = resolve\n fail = reject\n })\n\n const onSignal = () => {\n void cleanup(true).then(\n () => settle?.(),\n (error) => fail?.(error),\n )\n }\n\n try {\n server = createServer({\n port,\n storage,\n verifier,\n forwardTo: flags.forwardTo,\n onEvent: (event, result) => printEventCapturedBestEffort(terminal, event, result),\n })\n\n signals.on('SIGINT', onSignal)\n signals.on('SIGTERM', onSignal)\n listenersAttached = true\n\n await server.start()\n\n if (cleanedUp) {\n return await shutdown\n }\n\n terminal.printListenStarted({\n port: server.port,\n dbPath,\n verifier: verifier?.provider,\n forwardTo: flags.forwardTo,\n })\n\n return await shutdown\n } catch (error) {\n if (cleanedUp) {\n return await shutdown\n }\n\n await cleanup(false)\n throw error\n }\n}\n\nexport const listenCommand = new Command('listen')\n .description('Start receiving webhooks')\n .option('-p, --port <port>', 'Port to listen on', '4400')\n .option('--verify <provider>', 'Verify signatures (stripe)')\n .option('--secret <secret>', 'Webhook signing secret')\n .option('--forward-to <url>', 'Forward received webhooks to this URL')\n .action(async (options) => {\n const terminal = createTerminal()\n\n try {\n await runListen(options, { terminal })\n } catch (error) {\n terminal.printError(error instanceof Error ? error.message : String(error))\n\n process.exitCode = 1\n }\n })\n","import http from 'node:http'\nimport crypto from 'node:crypto'\nimport type { ReplayResult, VerificationResult, Verifier, WebhookEvent } from '../types.js'\nimport type { createStorage } from '../storage/index.js'\n\ntype Storage = ReturnType<typeof createStorage>\n\nexport interface ServerOptions {\n port: number\n storage: Storage\n verifier?: Verifier\n forwardTo?: string\n forwardTimeoutMs?: number\n maxBodyBytes?: number\n onEvent?: (event: WebhookEvent, result: VerificationResult | null) => void\n}\n\nexport interface Server {\n readonly port: number\n start(): Promise<void>\n stop(): Promise<void>\n}\n\n// Headers we strip before forwarding. This is the RFC 7230 section 6.1\n// hop-by-hop list plus host (fetch sets this from the destination URL) and\n// content-length (fetch recomputes this from the body).\nconst FORWARD_STRIP = new Set([\n 'connection',\n 'keep-alive',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n 'host',\n 'content-length',\n])\n\nconst DEFAULT_MAX_BODY_BYTES = 1024 * 1024\nconst DEFAULT_FORWARD_TIMEOUT_MS = 5000\n\nfunction forwardedStripSet(headers: Record<string, string>): Set<string> {\n const strip = new Set(FORWARD_STRIP)\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() !== 'connection') continue\n for (const token of value.split(/[,\\s]+/)) {\n const name = token.trim().toLowerCase()\n if (name) strip.add(name)\n }\n }\n return strip\n}\n\nfunction generateEventId(): string {\n return `evt_${crypto.randomBytes(12).toString('base64url')}`\n}\n\nclass PayloadTooLargeError extends Error {\n constructor(readonly maxBytes: number) {\n super(`payload too large: max ${maxBytes} bytes`)\n this.name = 'PayloadTooLargeError'\n }\n}\n\nfunction isPayloadTooLargeError(error: unknown): error is PayloadTooLargeError {\n return error instanceof PayloadTooLargeError\n}\n\nfunction requestSockets(req: http.IncomingMessage): NodeJS.EventEmitter[] {\n const sockets = new Set<NodeJS.EventEmitter>()\n sockets.add(req.socket)\n const proxiedSocket = (req.socket as typeof req.socket & { proxy?: NodeJS.EventEmitter | null })\n .proxy\n if (proxiedSocket) sockets.add(proxiedSocket)\n return [...sockets]\n}\n\nexport function readBody(req: http.IncomingMessage, maxBytes: number): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let totalBytes = 0\n let settled = false\n const sockets = requestSockets(req)\n\n const cleanup = () => {\n req.off('data', onData)\n req.off('end', onEnd)\n req.off('error', onError)\n for (const socket of sockets) {\n socket.off('close', onSocketClose)\n socket.off('error', onSocketError)\n }\n }\n\n const rejectOnce = (error: Error) => {\n if (settled) return\n settled = true\n cleanup()\n reject(error)\n }\n\n const resolveOnce = (body: string) => {\n if (settled) return\n settled = true\n cleanup()\n resolve(body)\n }\n\n const onData = (chunk: Buffer) => {\n totalBytes += chunk.length\n if (totalBytes > maxBytes) {\n req.resume()\n rejectOnce(new PayloadTooLargeError(maxBytes))\n return\n }\n chunks.push(chunk)\n }\n\n const onEnd = () => resolveOnce(Buffer.concat(chunks, totalBytes).toString('utf8'))\n const onError = (error: Error) => rejectOnce(error)\n const onSocketClose = () => rejectOnce(new Error('socket closed during request body'))\n const onSocketError = (error: Error) => rejectOnce(error)\n\n req.on('data', onData)\n req.on('end', onEnd)\n req.on('error', onError)\n for (const socket of sockets) {\n socket.on('close', onSocketClose)\n socket.on('error', onSocketError)\n }\n })\n}\n\nfunction headersToRecord(headers: http.IncomingHttpHeaders): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(headers)) {\n if (value === undefined) continue\n out[key] = Array.isArray(value) ? value.join(', ') : value\n }\n return out\n}\n\nexport function headersForForwarding(headers: Record<string, string>): Record<string, string> {\n const out: Record<string, string> = {}\n const strip = forwardedStripSet(headers)\n for (const [key, value] of Object.entries(headers)) {\n if (!strip.has(key.toLowerCase())) out[key] = value\n }\n return out\n}\n\ninterface ParsedEventPath {\n pathname: string\n search: string\n}\n\nfunction forwardPathname(targetPathname: string, incomingPathname: string): string {\n if (targetPathname === '/' || targetPathname === '') return incomingPathname || '/'\n if (incomingPathname === '/' || incomingPathname === '') return targetPathname\n const base = targetPathname.endsWith('/') ? targetPathname.slice(0, -1) : targetPathname\n const incoming = incomingPathname.startsWith('/') ? incomingPathname : `/${incomingPathname}`\n return `${base}${incoming}`\n}\n\nexport function parseEventPath(path: string): ParsedEventPath {\n if (/^[A-Za-z][A-Za-z\\d+.-]*:/.test(path)) {\n const parsed = new URL(path)\n return { pathname: parsed.pathname, search: parsed.search }\n }\n\n const queryIndex = path.indexOf('?')\n if (queryIndex === -1) {\n return { pathname: path, search: '' }\n }\n\n return {\n pathname: path.slice(0, queryIndex),\n search: path.slice(queryIndex),\n }\n}\n\nfunction mergeForwardSearch(targetSearch: string, incomingSearch: string): string {\n const merged = new URLSearchParams(incomingSearch)\n const trusted = new URLSearchParams(targetSearch)\n\n for (const key of new Set(trusted.keys())) {\n merged.delete(key)\n }\n for (const [key, value] of trusted) {\n merged.append(key, value)\n }\n\n const search = merged.toString()\n return search.length > 0 ? `?${search}` : ''\n}\n\nfunction isAbortError(error: unknown): boolean {\n return error instanceof Error && error.name === 'AbortError'\n}\n\nexport async function forwardEvent(\n targetUrl: string,\n event: WebhookEvent,\n timeoutMs = DEFAULT_FORWARD_TIMEOUT_MS,\n): Promise<ReplayResult> {\n const target = new URL(targetUrl)\n const destination = new URL(target.href)\n const parsedEventPath = parseEventPath(event.path)\n const controller = new AbortController()\n const timeout = setTimeout(() => controller.abort(), timeoutMs)\n\n destination.pathname = forwardPathname(destination.pathname, parsedEventPath.pathname)\n destination.search = mergeForwardSearch(destination.search, parsedEventPath.search)\n\n try {\n const hasBody = event.method !== 'GET' && event.method !== 'HEAD'\n const response = await fetch(destination, {\n method: event.method,\n headers: headersForForwarding(event.headers),\n body: hasBody ? event.body : undefined,\n signal: controller.signal,\n })\n\n return {\n status: response.status,\n body: await response.text(),\n }\n } catch (error) {\n if (isAbortError(error)) {\n throw new Error(`forward timed out after ${timeoutMs}ms`)\n }\n throw error\n } finally {\n clearTimeout(timeout)\n }\n}\n\nexport function createServer(opts: ServerOptions): Server {\n let boundPort = opts.port\n let httpServer: http.Server | null = null\n let isStarting = false\n const maxBodyBytes = opts.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES\n const forwardTimeoutMs = opts.forwardTimeoutMs ?? DEFAULT_FORWARD_TIMEOUT_MS\n\n const handleRequest = async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): Promise<void> => {\n const body = await readBody(req, maxBodyBytes)\n\n const event: WebhookEvent = {\n id: generateEventId(),\n timestamp: new Date().toISOString(),\n method: req.method ?? 'GET',\n path: req.url ?? '/',\n headers: headersToRecord(req.headers),\n body,\n }\n\n opts.storage.save(event)\n const verification = opts.verifier?.verify({ headers: event.headers, body: event.body }) ?? null\n opts.onEvent?.(event, verification)\n\n if (!opts.forwardTo) {\n res.statusCode = 200\n res.end('ok')\n return\n }\n\n try {\n const forwarded = await forwardEvent(opts.forwardTo, event, forwardTimeoutMs)\n res.statusCode = forwarded.status\n res.end(forwarded.body)\n } catch {\n res.statusCode = 502\n res.end('bad gateway')\n }\n }\n\n return {\n get port() {\n return boundPort\n },\n\n async start() {\n if (httpServer || isStarting) {\n throw new Error('server already started')\n }\n\n isStarting = true\n const server = http.createServer((req, res) => {\n handleRequest(req, res).catch((err: unknown) => {\n if (isPayloadTooLargeError(err)) {\n res.statusCode = 413\n res.end(err.message)\n return\n }\n res.statusCode = 500\n res.end(err instanceof Error ? err.message : String(err))\n })\n })\n httpServer = server\n\n try {\n await new Promise<void>((resolve, reject) => {\n const onError = (err: Error) => {\n server.off('error', onError)\n if (httpServer === server) httpServer = null\n boundPort = opts.port\n isStarting = false\n reject(err)\n }\n\n server.once('error', onError)\n server.listen(opts.port, '127.0.0.1', () => {\n server.off('error', onError)\n const addr = server.address()\n if (addr && typeof addr !== 'string') {\n boundPort = addr.port\n }\n isStarting = false\n resolve()\n })\n })\n } catch (err) {\n if (httpServer === server) httpServer = null\n boundPort = opts.port\n isStarting = false\n throw err\n }\n },\n\n async stop() {\n if (!httpServer) return\n const server = httpServer\n await new Promise<void>((resolve, reject) => {\n server.close((err) => {\n if (httpServer === server) httpServer = null\n boundPort = opts.port\n isStarting = false\n if (err) {\n reject(err)\n return\n }\n resolve()\n })\n })\n },\n }\n}\n","import os from 'node:os'\nimport fs from 'node:fs'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport type * as sqlite from 'node:sqlite'\nimport { eventRowSchema, webhookEventSchema, type EventRow, type WebhookEvent } from '../types.js'\n\nconst require = createRequire(import.meta.url)\n\n// tsup/esbuild currently rewrites a static `node:sqlite` import to `sqlite`,\n// which breaks the built CLI. Resolve it at runtime so the core module specifier\n// survives the bundle unchanged.\nconst { DatabaseSync } = require('node:' + 'sqlite') as typeof sqlite\n\nexport function defaultDbPath(): string {\n return path.join(os.homedir(), '.hooklens', 'events.db')\n}\n\nfunction rowToEvent(row: EventRow): WebhookEvent {\n return webhookEventSchema.parse({\n id: row.id,\n timestamp: row.timestamp,\n method: row.method,\n path: row.path,\n headers: JSON.parse(row.headers),\n body: row.body,\n })\n}\n\nexport function createStorage(dbPath: string) {\n fs.mkdirSync(path.dirname(dbPath), { recursive: true })\n const db = new DatabaseSync(dbPath)\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS events (\n id TEXT PRIMARY KEY,\n timestamp TEXT NOT NULL,\n method TEXT NOT NULL,\n path TEXT NOT NULL,\n headers TEXT NOT NULL,\n body TEXT NOT NULL\n )\n `)\n\n const insertStmt = db.prepare(\n `INSERT OR REPLACE INTO events (id, timestamp, method, path, headers, body)\n VALUES (?, ?, ?, ?, ?, ?)`,\n )\n\n const getStmt = db.prepare(`SELECT * FROM events WHERE id = ?`)\n const listAllStmt = db.prepare(`SELECT * FROM events ORDER BY timestamp DESC`)\n const listLimitedStmt = db.prepare(`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`)\n const clearStmt = db.prepare(`DELETE FROM events`)\n\n return {\n save(event: WebhookEvent): void {\n insertStmt.run(\n event.id,\n event.timestamp,\n event.method,\n event.path,\n JSON.stringify(event.headers),\n event.body,\n )\n },\n\n load(id: string): WebhookEvent | null {\n const raw = getStmt.get(id)\n if (!raw) return null\n const row = eventRowSchema.parse(raw)\n return rowToEvent(row)\n },\n\n list(limit?: number): WebhookEvent[] {\n if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {\n throw new Error(`Invalid limit: must be a positive integer, got ${limit}`)\n }\n const raw = limit === undefined ? listAllStmt.all() : listLimitedStmt.all(limit)\n const rows = raw.map((r) => eventRowSchema.parse(r))\n return rows.map(rowToEvent)\n },\n\n clear(): void {\n clearStmt.run()\n },\n\n close(): void {\n db.close()\n },\n }\n}\n","import { z } from 'zod'\n\n// A webhook event as it lives in memory and is exposed to the rest of the app.\n// Headers are a parsed object here -- on disk they're stored as a JSON string.\nexport const webhookEventSchema = z.object({\n id: z.string(),\n timestamp: z.string(),\n method: z.string(),\n path: z.string(),\n headers: z.record(z.string(), z.string()),\n body: z.string(),\n})\n\nexport type WebhookEvent = z.infer<typeof webhookEventSchema>\n\n// The shape of a row read directly from the SQLite events table.\n// headers is a JSON string at this layer; rowToEvent parses it.\nexport const eventRowSchema = z.object({\n id: z.string(),\n timestamp: z.string(),\n method: z.string(),\n path: z.string(),\n headers: z.string(),\n body: z.string(),\n})\n\nexport type EventRow = z.infer<typeof eventRowSchema>\n\nexport const verificationResultSchema = z.object({\n valid: z.boolean(),\n provider: z.string(),\n message: z.string(),\n code: z.enum([\n 'valid',\n 'missing_header',\n 'malformed_header',\n 'expired_timestamp',\n 'signature_mismatch',\n 'body_mutated',\n ]),\n})\n\nexport type VerificationResult = z.infer<typeof verificationResultSchema>\n\nexport const replayResultSchema = z.object({\n status: z.number().int(),\n body: z.string(),\n})\n\nexport type ReplayResult = z.infer<typeof replayResultSchema>\n\n/** Provider signature verifier. See CONTRIBUTING.md → Adding a provider. */\nexport interface Verifier {\n readonly provider: string\n verify(event: { headers: Record<string, string>; body: string }): VerificationResult\n}\n","import chalk from 'chalk'\nimport type { ReplayResult, VerificationResult, WebhookEvent } from '../types.js'\n\nexport interface ListenStartedInfo {\n port: number\n dbPath: string\n verifier?: string\n forwardTo?: string\n}\n\nexport interface TerminalUI {\n printListenStarted(info: ListenStartedInfo): void\n printEventCaptured(event: WebhookEvent, result: VerificationResult | null): void\n printEventList(events: WebhookEvent[]): void\n printReplayResult(result: ReplayResult): void\n printListenStopped(): void\n printError(message: string): void\n}\n\nfunction writeLine(stream: NodeJS.WriteStream, line: string): void {\n stream.write(`${line}\\n`)\n}\n\nfunction verificationLabel(result: VerificationResult | null): string {\n if (!result) return chalk.cyan('RECV')\n return result.valid ? chalk.green('PASS') : chalk.red('FAIL')\n}\n\nexport function createTerminal(\n stdout: NodeJS.WriteStream = process.stdout,\n stderr: NodeJS.WriteStream = process.stderr,\n): TerminalUI {\n return {\n printListenStarted(info) {\n writeLine(\n stdout,\n `${chalk.bold('Listening on')} ${chalk.cyan(`http://127.0.0.1:${info.port}`)}`,\n )\n\n writeLine(stdout, `Verifier: ${info.verifier ?? 'none'}`)\n writeLine(stdout, `Forwarding to: ${info.forwardTo ?? 'disabled'}`)\n writeLine(stdout, `Storage: ${info.dbPath}`)\n },\n\n printEventCaptured(event, result) {\n const label = verificationLabel(result)\n const summary = `${label} ${chalk.bold(event.id)} ${event.method} ${event.path}`\n\n if (!result) {\n writeLine(stdout, summary)\n return\n }\n\n writeLine(stdout, `${summary} ${result.message}`)\n },\n\n printEventList(events) {\n if (!events.length) {\n writeLine(stdout, chalk.dim('No stored events.'))\n return\n }\n\n for (const event of events) {\n const row = `${chalk.dim(event.timestamp)} ${chalk.cyan(event.method)} ${chalk.bold(event.id)} ${event.path}`\n writeLine(stdout, row)\n }\n },\n\n printReplayResult(result) {\n const summary = `${chalk.bold('Replay response:')} ${chalk.cyan(String(result.status))}`\n\n if (!result.body) {\n writeLine(stdout, summary)\n return\n }\n\n writeLine(stdout, `${summary} ${result.body}`)\n },\n\n printListenStopped() {\n writeLine(stdout, chalk.dim('Stopped listening.'))\n },\n\n printError(message) {\n writeLine(stderr, chalk.red(message))\n },\n }\n}\n","import crypto from 'node:crypto'\nimport type { VerificationResult, Verifier } from '../types.js'\nimport { getHeaderCaseInsensitive } from './headers.js'\n\nexport interface VerifyStripeOptions {\n payload: string\n header: string | null | undefined\n secret: string\n tolerance?: number\n now?: () => number\n}\n\nconst DEFAULT_TOLERANCE_SECONDS = 300\nconst PROVIDER = 'stripe'\n\ninterface ParsedHeader {\n timestamp: number\n signatures: string[]\n}\n\nfunction parseHeader(header: string): ParsedHeader | null {\n let timestamp: number | null = null\n const signatures: string[] = []\n\n for (const part of header.split(',')) {\n const eqIdx = part.indexOf('=')\n if (eqIdx === -1) return null\n\n const key = part.slice(0, eqIdx)\n const value = part.slice(eqIdx + 1)\n\n if (key === 't') {\n if (!/^\\d+$/.test(value)) return null\n timestamp = Number(value)\n } else if (key === 'v1') {\n if (value.length === 0) return null\n signatures.push(value)\n }\n }\n\n if (timestamp === null || signatures.length === 0) return null\n return { timestamp, signatures }\n}\n\nfunction computeHmac(secret: string, signedPayload: string): string {\n return crypto.createHmac('sha256', secret).update(signedPayload).digest('hex')\n}\n\nfunction constantTimeMatch(expected: string, candidates: string[]): boolean {\n const expectedBuf = Buffer.from(expected, 'utf8')\n for (const candidate of candidates) {\n if (candidate.length !== expected.length) continue\n const candidateBuf = Buffer.from(candidate, 'utf8')\n if (crypto.timingSafeEqual(expectedBuf, candidateBuf)) return true\n }\n return false\n}\n\nfunction tryCanonicalForm(payload: string): string | null {\n try {\n const canonical = JSON.stringify(JSON.parse(payload))\n return canonical === payload ? null : canonical\n } catch {\n return null\n }\n}\n\nfunction success(message: string): VerificationResult {\n return { valid: true, provider: PROVIDER, code: 'valid', message }\n}\n\nfunction failure(\n code: Exclude<VerificationResult['code'], 'valid'>,\n message: string,\n): VerificationResult {\n return { valid: false, provider: PROVIDER, code, message }\n}\n\nexport function verifyStripeSignature(opts: VerifyStripeOptions): VerificationResult {\n if (!opts.header) {\n return failure(\n 'missing_header',\n 'stripe-signature header not found. Is this actually from Stripe?',\n )\n }\n\n const parsed = parseHeader(opts.header)\n if (!parsed) {\n return failure(\n 'malformed_header',\n 'stripe-signature header is malformed. Expected format: t=timestamp,v1=signature',\n )\n }\n\n const tolerance = opts.tolerance ?? DEFAULT_TOLERANCE_SECONDS\n const nowMs = (opts.now ?? Date.now)()\n const ageSeconds = Math.floor(nowMs / 1000) - parsed.timestamp\n\n if (ageSeconds > tolerance) {\n const minutes = Math.floor(ageSeconds / 60)\n return failure(\n 'expired_timestamp',\n `Timestamp is ${minutes} minutes old. Event expired or your clock is drifting.`,\n )\n }\n\n const signedPayload = `${parsed.timestamp}.${opts.payload}`\n const expected = computeHmac(opts.secret, signedPayload)\n\n if (constantTimeMatch(expected, parsed.signatures)) {\n return success('Signature verified.')\n }\n\n const canonical = tryCanonicalForm(opts.payload)\n if (canonical !== null) {\n const expectedCanonical = computeHmac(opts.secret, `${parsed.timestamp}.${canonical}`)\n if (constantTimeMatch(expectedCanonical, parsed.signatures)) {\n return failure(\n 'body_mutated',\n 'Signature mismatch with correct secret. Body was likely parsed and re-serialized by your framework.',\n )\n }\n }\n\n return failure(\n 'signature_mismatch',\n 'Signature mismatch. Check your webhook secret matches the Stripe dashboard.',\n )\n}\n\nexport interface StripeVerifierOptions {\n secret: string\n tolerance?: number\n}\n\nexport function createStripeVerifier(opts: StripeVerifierOptions): Verifier {\n return {\n provider: PROVIDER,\n verify: (event) =>\n verifyStripeSignature({\n payload: event.body,\n header: getHeaderCaseInsensitive(event.headers, 'stripe-signature'),\n secret: opts.secret,\n tolerance: opts.tolerance,\n }),\n }\n}\n","export function getHeaderCaseInsensitive(\n headers: Record<string, string>,\n name: string,\n): string | undefined {\n const expected = name.toLowerCase()\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === expected) return value\n }\n return undefined\n}\n","import { Command } from 'commander'\nimport { createStorage, defaultDbPath } from '../storage/index.js'\nimport { createTerminal, type TerminalUI } from '../ui/terminal.js'\n\nexport interface ListFlags {\n limit?: string | number\n}\n\nexport interface ListDeps {\n terminal?: TerminalUI\n}\n\nfunction parseLimit(limit: string | number | undefined): number {\n const raw = limit\n const parsed = typeof raw === 'number' ? raw : Number(raw)\n\n if (!Number.isInteger(parsed) || parsed <= 0) {\n throw new Error(`Invalid limit \"${raw}\". Expected a positive integer.`)\n }\n\n return parsed\n}\n\nexport async function runList(flags: ListFlags, deps: ListDeps = {}): Promise<void> {\n const limit = parseLimit(flags.limit ?? '20')\n const terminal = deps.terminal ?? createTerminal()\n const storage = createStorage(defaultDbPath())\n\n try {\n const events = storage.list(limit)\n terminal.printEventList(events)\n } finally {\n storage.close()\n }\n}\n\nexport const listCommand = new Command('list')\n .description('Show received webhook events')\n .option('-n, --limit <count>', 'Number of events to show', '20')\n .action(async (options) => {\n const terminal = createTerminal()\n\n try {\n await runList(options, { terminal })\n } catch (error) {\n terminal.printError(error instanceof Error ? error.message : String(error))\n process.exitCode = 1\n }\n })\n","import { Command } from 'commander'\nimport { forwardEvent } from '../server/index.js'\nimport { createStorage, defaultDbPath } from '../storage/index.js'\nimport { createTerminal, type TerminalUI } from '../ui/terminal.js'\n\nconst DEFAULT_REPLAY_TARGET_URL = 'http://localhost:3000/webhook'\n\nexport interface ReplayFlags {\n to?: string\n}\n\nexport interface ReplayDeps {\n terminal?: TerminalUI\n}\n\nfunction parseTargetUrl(targetUrl: string | undefined): string {\n const raw = targetUrl ?? DEFAULT_REPLAY_TARGET_URL\n\n try {\n return new URL(raw).href\n } catch {\n throw new Error(`Invalid target URL \"${raw}\".`)\n }\n}\n\nexport async function runReplay(\n eventId: string,\n flags: ReplayFlags,\n deps: ReplayDeps = {},\n): Promise<void> {\n const targetUrl = parseTargetUrl(flags.to)\n const terminal = deps.terminal ?? createTerminal()\n const storage = createStorage(defaultDbPath())\n\n try {\n const event = storage.load(eventId)\n\n if (!event) {\n throw new Error(`Event \"${eventId}\" not found.`)\n }\n\n try {\n const result = await forwardEvent(targetUrl, event)\n const body = result.body.length <= 200 ? result.body : `${result.body.slice(0, 197)}...`\n\n terminal.printReplayResult({\n status: result.status,\n body,\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(`Failed to replay \"${eventId}\" to ${targetUrl}: ${message}`)\n }\n } finally {\n storage.close()\n }\n}\n\nexport const replayCommand = new Command('replay')\n .description('Replay a stored webhook event')\n .argument('<event-id>', 'ID of the event to replay')\n .option('--to <url>', 'Target URL to send the event to', DEFAULT_REPLAY_TARGET_URL)\n .action(async (eventId, options) => {\n const terminal = createTerminal()\n\n try {\n await runReplay(eventId, options, { terminal })\n } catch (error) {\n terminal.printError(error instanceof Error ? error.message : String(error))\n process.exitCode = 1\n }\n })\n"],"mappings":"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","names":["Command","path","require","crypto","crypto","Command","Command","Command","Command","Command"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hooklens",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Inspect, verify, and replay webhooks from your terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hooklens": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsup --watch",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"test:coverage": "vitest run --coverage",
|
|
16
|
+
"lint": "eslint src/ tests/",
|
|
17
|
+
"lint:fix": "eslint src/ tests/ --fix",
|
|
18
|
+
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
|
|
19
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"prepare": "husky"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"webhook",
|
|
26
|
+
"debug",
|
|
27
|
+
"stripe",
|
|
28
|
+
"signature",
|
|
29
|
+
"replay",
|
|
30
|
+
"cli",
|
|
31
|
+
"developer-tools"
|
|
32
|
+
],
|
|
33
|
+
"author": "Ilia Goginashvili",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/Ilia01/hooklens.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/Ilia01/hooklens/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/Ilia01/hooklens#readme",
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=24.0.0"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE"
|
|
50
|
+
],
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.0.0",
|
|
53
|
+
"eslint": "^9.0.0",
|
|
54
|
+
"husky": "^9.1.7",
|
|
55
|
+
"lint-staged": "^16.4.0",
|
|
56
|
+
"prettier": "^3.4.0",
|
|
57
|
+
"stripe": "^22.0.0",
|
|
58
|
+
"tsup": "^8.0.0",
|
|
59
|
+
"typescript": "^5.7.0",
|
|
60
|
+
"typescript-eslint": "^8.0.0",
|
|
61
|
+
"vitest": "^3.0.0"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"chalk": "^5.4.0",
|
|
65
|
+
"commander": "^13.0.0",
|
|
66
|
+
"zod": "^4.3.6"
|
|
67
|
+
},
|
|
68
|
+
"lint-staged": {
|
|
69
|
+
"*.{ts,tsx}": [
|
|
70
|
+
"prettier --write",
|
|
71
|
+
"eslint --fix"
|
|
72
|
+
],
|
|
73
|
+
"*.{json,md,yml,yaml}": [
|
|
74
|
+
"prettier --write"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|