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 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 opens a Server-Sent Events (SSE) connection to receive incoming requests in real time.
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
- The connection auto-reconnects with exponential backoff if interrupted.
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> Local port to forward to (default: 3000)
67
- --bin <slug> Bin slug to listen on (optional)
68
- --name <s> Human-readable tunnel name (optional)
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
- * SSE-based tunnel that receives webhook requests from Reqly and
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/connect SSE stream of incoming requests
8
- * 3. For each event: forward to localhost, then
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
- * Reconnects automatically with exponential backoff + jitter.
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
- reportResponse(serverUrl, token, captureId, result).catch(() => {});
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
- // Poll every 1 second
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
- if (!config.token) {
286
- console.log(`${RED}Not logged in.${RESET} Run ${BOLD}reqly login${RESET} first.`);
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. Connect SSE and start forwarding
320
- await pollForCaptures(serverUrl, token, tunnelId, localOrigin);
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.2.3",
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": "./bin/reqly.mjs"
7
+ "reqly": "bin/reqly.mjs"
8
8
  },
9
9
  "files": [
10
10
  "bin/",