reqly-cli 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/bin/reqly.mjs +9 -4
- package/lib/tunnel.mjs +36 -24
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ Start a tunnel that forwards captured requests to your local server.
|
|
|
25
25
|
reqly tunnel --port 3000
|
|
26
26
|
reqly tunnel --port 8080 --bin my-webhook
|
|
27
27
|
reqly tunnel --port 3000 --name "stripe hooks"
|
|
28
|
+
reqly tunnel --port 3000 --passthrough
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
| Option | Description | Default |
|
|
@@ -32,6 +33,7 @@ reqly tunnel --port 3000 --name "stripe hooks"
|
|
|
32
33
|
| `--port <n>` | Local port to forward to | `3000` |
|
|
33
34
|
| `--bin <slug>` | Bin slug to listen on | assigned by server |
|
|
34
35
|
| `--name <s>` | Human-readable tunnel name | `cli-<port>` |
|
|
36
|
+
| `--passthrough` | Reverse-proxy mode — relay your local server's real response back to the caller (serves HTML, redirects, etc., not just webhooks) | off |
|
|
35
37
|
|
|
36
38
|
### `reqly login`
|
|
37
39
|
|
|
@@ -56,11 +58,13 @@ Show version.
|
|
|
56
58
|
## How it works
|
|
57
59
|
|
|
58
60
|
1. The CLI registers a tunnel with the Reqly server.
|
|
59
|
-
2. It
|
|
61
|
+
2. It polls the server for incoming requests (`GET /api/tunnel/poll`). Polling is used instead of streaming because it survives reverse proxies (Traefik, nginx) that buffer or drop long-lived connections.
|
|
60
62
|
3. Each request is forwarded to your local server (`http://localhost:<port>`).
|
|
61
63
|
4. The response from your local server is reported back to Reqly so you can inspect it in the dashboard.
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
In `--passthrough` mode, the caller is held open until your local server responds, and that real response is relayed back to them — so the public URL behaves like a reverse proxy (HTML pages, redirects, status codes), not just a webhook collector.
|
|
66
|
+
|
|
67
|
+
The poll loop keeps running through transient network errors and resumes automatically once the server is reachable again.
|
|
64
68
|
|
|
65
69
|
## Configuration
|
|
66
70
|
|
package/bin/reqly.mjs
CHANGED
|
@@ -63,9 +63,11 @@ ${BOLD}Commands:${RESET}
|
|
|
63
63
|
status Show connection status and config info
|
|
64
64
|
|
|
65
65
|
${BOLD}Tunnel options:${RESET}
|
|
66
|
-
--port <n>
|
|
67
|
-
--bin <slug>
|
|
68
|
-
--name <s>
|
|
66
|
+
--port <n> Local port to forward to (default: 3000)
|
|
67
|
+
--bin <slug> Bin slug to listen on (optional)
|
|
68
|
+
--name <s> Human-readable tunnel name (optional)
|
|
69
|
+
--passthrough Reverse-proxy mode: relay the local server's real response
|
|
70
|
+
back to the caller (serves HTML/redirects, not just webhooks)
|
|
69
71
|
|
|
70
72
|
${BOLD}Global options:${RESET}
|
|
71
73
|
--help Show this help message
|
|
@@ -134,13 +136,16 @@ async function main() {
|
|
|
134
136
|
const port = parseInt(getFlag("--port") || config.defaultPort, 10);
|
|
135
137
|
const bin = getFlag("--bin");
|
|
136
138
|
const name = getFlag("--name");
|
|
139
|
+
const url = getFlag("--url");
|
|
140
|
+
const token = getFlag("--token");
|
|
141
|
+
const passthrough = hasFlag("--passthrough");
|
|
137
142
|
|
|
138
143
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
139
144
|
console.log(`${RED}Invalid port number.${RESET} Use --port <1-65535>`);
|
|
140
145
|
process.exit(1);
|
|
141
146
|
}
|
|
142
147
|
|
|
143
|
-
await startTunnel({ port, bin, name });
|
|
148
|
+
await startTunnel({ port, bin, name, url, token, passthrough });
|
|
144
149
|
break;
|
|
145
150
|
}
|
|
146
151
|
|
package/lib/tunnel.mjs
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Polling-based tunnel that receives webhook requests from Reqly and
|
|
3
3
|
* forwards them to a local dev server.
|
|
4
4
|
*
|
|
5
|
+
* Polling is used instead of SSE because it survives reverse proxies
|
|
6
|
+
* (Traefik, nginx) that buffer or drop long-lived streaming connections.
|
|
7
|
+
*
|
|
5
8
|
* Flow:
|
|
6
9
|
* 1. POST /api/tunnel/register → register this tunnel
|
|
7
|
-
* 2. GET /api/tunnel/
|
|
8
|
-
* 3. For each
|
|
10
|
+
* 2. GET /api/tunnel/poll → fetch captures newer than the cursor
|
|
11
|
+
* 3. For each capture: forward to localhost, then
|
|
9
12
|
* POST /api/tunnel/response → report the response back
|
|
10
13
|
*
|
|
11
|
-
*
|
|
14
|
+
* The poll loop keeps running through transient errors; it logs after
|
|
15
|
+
* repeated failures rather than giving up.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import { loadConfig } from "./config.mjs";
|
|
@@ -22,11 +26,6 @@ const RED = "\x1b[31m";
|
|
|
22
26
|
const CYAN = "\x1b[36m";
|
|
23
27
|
const RESET = "\x1b[0m";
|
|
24
28
|
|
|
25
|
-
// ── Backoff settings ────────────────────────────────────────────────
|
|
26
|
-
const BACKOFF_INITIAL_MS = 1000;
|
|
27
|
-
const BACKOFF_MAX_MS = 60_000;
|
|
28
|
-
const BACKOFF_FACTOR = 2;
|
|
29
|
-
|
|
30
29
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
31
30
|
|
|
32
31
|
function timestamp() {
|
|
@@ -162,14 +161,14 @@ function parseSSELine(line, current) {
|
|
|
162
161
|
* Register this tunnel with the server.
|
|
163
162
|
* Returns { tunnelId, binSlug, binUrl }.
|
|
164
163
|
*/
|
|
165
|
-
async function registerTunnel(serverUrl, token, name, port, binSlug) {
|
|
164
|
+
async function registerTunnel(serverUrl, token, name, port, binSlug, passthrough) {
|
|
166
165
|
const res = await fetch(`${serverUrl}/api/tunnel/register`, {
|
|
167
166
|
method: "POST",
|
|
168
167
|
headers: {
|
|
169
168
|
"Content-Type": "application/json",
|
|
170
169
|
Authorization: `Bearer ${token}`,
|
|
171
170
|
},
|
|
172
|
-
body: JSON.stringify({ token, name, port, binSlug }),
|
|
171
|
+
body: JSON.stringify({ token, name, port, binSlug, passthrough: !!passthrough }),
|
|
173
172
|
});
|
|
174
173
|
|
|
175
174
|
if (!res.ok) {
|
|
@@ -184,7 +183,7 @@ async function registerTunnel(serverUrl, token, name, port, binSlug) {
|
|
|
184
183
|
* Poll for new captures and forward them.
|
|
185
184
|
* More reliable than SSE through reverse proxies (Traefik, nginx).
|
|
186
185
|
*/
|
|
187
|
-
async function pollForCaptures(serverUrl, token, tunnelId, localOrigin) {
|
|
186
|
+
async function pollForCaptures(serverUrl, token, tunnelId, localOrigin, pollIntervalMs = 1000) {
|
|
188
187
|
let aborted = false;
|
|
189
188
|
let lastSeen = new Date().toISOString();
|
|
190
189
|
let consecutiveErrors = 0;
|
|
@@ -244,7 +243,9 @@ async function pollForCaptures(serverUrl, token, tunnelId, localOrigin) {
|
|
|
244
243
|
`${DIM}[${timestamp()}]${RESET} ${color}\u2190 ${result.status} ${result.statusText}${RESET} ${DIM}(${result.durationMs}ms)${RESET}`
|
|
245
244
|
);
|
|
246
245
|
|
|
247
|
-
|
|
246
|
+
// Await the report so a passthrough request held open on the server
|
|
247
|
+
// is released as soon as possible (non-fatal if it fails).
|
|
248
|
+
await reportResponse(serverUrl, token, captureId, result).catch(() => {});
|
|
248
249
|
|
|
249
250
|
// Update cursor
|
|
250
251
|
if (capture.createdAt && capture.createdAt > lastSeen) {
|
|
@@ -264,8 +265,7 @@ async function pollForCaptures(serverUrl, token, tunnelId, localOrigin) {
|
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
if (aborted) return;
|
|
267
|
-
|
|
268
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
268
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
@@ -279,16 +279,16 @@ async function pollForCaptures(serverUrl, token, tunnelId, localOrigin) {
|
|
|
279
279
|
* @param {string} [opts.bin] - Bin slug to listen on (optional, server can assign)
|
|
280
280
|
* @param {string} [opts.name] - Human-readable tunnel name
|
|
281
281
|
*/
|
|
282
|
-
export async function startTunnel({ port, bin, name }) {
|
|
282
|
+
export async function startTunnel({ port, bin, name, url, token: tokenOverride, passthrough }) {
|
|
283
283
|
const config = loadConfig();
|
|
284
284
|
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
const serverUrl = url || config.url;
|
|
286
|
+
const token = tokenOverride || config.token;
|
|
287
|
+
|
|
288
|
+
if (!token) {
|
|
289
|
+
console.log(`${RED}Not logged in.${RESET} Run ${BOLD}reqly login${RESET} first, or pass ${BOLD}--token${RESET}.`);
|
|
287
290
|
process.exit(1);
|
|
288
291
|
}
|
|
289
|
-
|
|
290
|
-
const serverUrl = config.url;
|
|
291
|
-
const token = config.token;
|
|
292
292
|
const localOrigin = `http://localhost:${port}`;
|
|
293
293
|
|
|
294
294
|
// 1. Register the tunnel
|
|
@@ -300,7 +300,8 @@ export async function startTunnel({ port, bin, name }) {
|
|
|
300
300
|
token,
|
|
301
301
|
name || `cli-${port}`,
|
|
302
302
|
port,
|
|
303
|
-
bin
|
|
303
|
+
bin,
|
|
304
|
+
passthrough
|
|
304
305
|
);
|
|
305
306
|
console.log(` ${GREEN}done${RESET}`);
|
|
306
307
|
} catch (err) {
|
|
@@ -315,7 +316,18 @@ export async function startTunnel({ port, bin, name }) {
|
|
|
315
316
|
|
|
316
317
|
// 2. Print banner
|
|
317
318
|
printBanner(publicUrl, localOrigin, true);
|
|
319
|
+
if (passthrough) {
|
|
320
|
+
console.log(
|
|
321
|
+
`${DIM}[${timestamp()}]${RESET} ${CYAN}Passthrough mode${RESET} ${DIM}— relaying real responses (serves HTML/redirects)${RESET}`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
318
324
|
|
|
319
|
-
// 3.
|
|
320
|
-
await pollForCaptures(
|
|
325
|
+
// 3. Start forwarding. Passthrough holds the caller open, so poll faster.
|
|
326
|
+
await pollForCaptures(
|
|
327
|
+
serverUrl,
|
|
328
|
+
token,
|
|
329
|
+
tunnelId,
|
|
330
|
+
localOrigin,
|
|
331
|
+
passthrough ? 250 : 1000
|
|
332
|
+
);
|
|
321
333
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reqly-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Reqly CLI — tunnel webhooks to your local dev server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"reqly": "
|
|
7
|
+
"reqly": "bin/reqly.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|