tunnelhook 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +10 -1
  3. package/src/index.tsx +86 -17
package/README.md CHANGED
@@ -39,7 +39,7 @@ This will:
39
39
 
40
40
  Your webhook URL will be:
41
41
  ```
42
- https://tunnelhook-server-shkumbinhasani.shkumbinhasani20001439.workers.dev/hooks/stripe-dev
42
+ https://api.tunnelhook.com/hooks/stripe-dev
43
43
  ```
44
44
 
45
45
  ### Interactive mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tunnelhook",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CLI tool for receiving and forwarding webhooks via TunnelHook",
5
5
  "type": "module",
6
6
  "module": "src/index.tsx",
@@ -24,6 +24,15 @@
24
24
  "forward"
25
25
  ],
26
26
  "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/plutolaboratories/tunnelhook.git",
30
+ "directory": "apps/tui"
31
+ },
32
+ "publishConfig": {
33
+ "provenance": true,
34
+ "access": "public"
35
+ },
27
36
  "engines": {
28
37
  "bun": ">=1.0.0"
29
38
  },
package/src/index.tsx CHANGED
@@ -19,8 +19,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
19
19
  // ---------------------------------------------------------------------------
20
20
 
21
21
  const SERVER_URL =
22
- process.env.TUNNELHOOK_SERVER_URL ??
23
- "https://tunnelhook-server-shkumbinhasani.shkumbinhasani20001439.workers.dev";
22
+ process.env.TUNNELHOOK_SERVER_URL ?? "https://api.tunnelhook.com";
24
23
  const WS_URL = SERVER_URL.replace(/^http/, "ws");
25
24
 
26
25
  // ---------------------------------------------------------------------------
@@ -166,10 +165,13 @@ function loadSession(): string | null {
166
165
  function saveSession(cookies: string): void {
167
166
  try {
168
167
  if (!existsSync(CONFIG_DIR)) {
169
- mkdirSync(CONFIG_DIR, { recursive: true });
168
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
170
169
  }
171
170
  const data: SessionData = { cookies, serverUrl: SERVER_URL };
172
- writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2), "utf-8");
171
+ writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2), {
172
+ encoding: "utf-8",
173
+ mode: 0o600,
174
+ });
173
175
  } catch {
174
176
  // Non-critical — session just won't persist
175
177
  }
@@ -382,9 +384,39 @@ async function handleLoginCommand(): Promise<never> {
382
384
  console.log(`Server: ${SERVER_URL}\n`);
383
385
 
384
386
  const email = await ask("Email: ");
385
- const password = await ask("Password: ");
386
387
  rl.close();
387
388
 
389
+ // Read password with masked input (echo asterisks instead of plaintext)
390
+ const password = await new Promise<string>((resolve) => {
391
+ process.stdout.write("Password: ");
392
+ let pwd = "";
393
+ process.stdin.setRawMode(true);
394
+ process.stdin.resume();
395
+ process.stdin.setEncoding("utf8");
396
+ const onData = (ch: string) => {
397
+ if (ch === "\r" || ch === "\n") {
398
+ process.stdin.setRawMode(false);
399
+ process.stdin.pause();
400
+ process.stdin.removeListener("data", onData);
401
+ process.stdout.write("\n");
402
+ resolve(pwd);
403
+ } else if (ch === "\u007F" || ch === "\b") {
404
+ if (pwd.length > 0) {
405
+ pwd = pwd.slice(0, -1);
406
+ process.stdout.write("\b \b");
407
+ }
408
+ } else if (ch === "\u0003") {
409
+ // Ctrl+C
410
+ process.stdout.write("\n");
411
+ process.exit(0);
412
+ } else {
413
+ pwd += ch;
414
+ process.stdout.write("*");
415
+ }
416
+ };
417
+ process.stdin.on("data", onData);
418
+ });
419
+
388
420
  console.log("\nSigning in...");
389
421
  const result = await signIn(email, password);
390
422
 
@@ -673,12 +705,30 @@ function LoginScreen({ onLogin }: { onLogin: () => void }) {
673
705
  setFocusField((prev: "email" | "password") =>
674
706
  prev === "email" ? "password" : "email"
675
707
  );
708
+ return;
676
709
  }
677
710
  if ((key.name === "enter" || key.name === "return") && !loading) {
678
711
  handleSubmit();
712
+ return;
713
+ }
714
+
715
+ // Manually handle password input since we display masked characters
716
+ if (focusField === "password") {
717
+ if (key.name === "backspace") {
718
+ setPassword((prev) => prev.slice(0, -1));
719
+ } else if (
720
+ key.sequence &&
721
+ key.sequence.length === 1 &&
722
+ !key.ctrl &&
723
+ !key.meta
724
+ ) {
725
+ setPassword((prev) => prev + key.sequence);
726
+ }
679
727
  }
680
728
  });
681
729
 
730
+ const maskedPassword = "*".repeat(password.length);
731
+
682
732
  return (
683
733
  <box
684
734
  alignItems="center"
@@ -715,16 +765,15 @@ function LoginScreen({ onLogin }: { onLogin: () => void }) {
715
765
  />
716
766
 
717
767
  <text fg={COLORS.text}>Password</text>
718
- <input
719
- backgroundColor={COLORS.bg}
720
- focused={focusField === "password"}
721
- focusedBackgroundColor="#1c2128"
722
- onChange={setPassword}
723
- placeholder="password"
724
- textColor={COLORS.text}
725
- value={password}
768
+ <box
769
+ backgroundColor={focusField === "password" ? "#1c2128" : COLORS.bg}
770
+ height={1}
726
771
  width={40}
727
- />
772
+ >
773
+ <text fg={password.length > 0 ? COLORS.text : COLORS.textDim}>
774
+ {password.length > 0 ? maskedPassword : "password"}
775
+ </text>
776
+ </box>
728
777
 
729
778
  {error ? <text fg={COLORS.red}>{error}</text> : null}
730
779
  {loading ? (
@@ -1138,6 +1187,10 @@ function MonitorScreen({
1138
1187
  const [selectedIndex, setSelectedIndex] = useState(0);
1139
1188
  const [connected, setConnected] = useState(false);
1140
1189
  const [wsStatus, setWsStatus] = useState<string>("connecting...");
1190
+ const [reconnectAttempt, setReconnectAttempt] = useState(0);
1191
+ const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
1192
+ null
1193
+ );
1141
1194
  const wsRef = useRef<WebSocket | null>(null);
1142
1195
 
1143
1196
  // Connect to WebSocket as a machine
@@ -1159,6 +1212,7 @@ function MonitorScreen({
1159
1212
  ws.addEventListener("open", () => {
1160
1213
  setConnected(true);
1161
1214
  setWsStatus("connected");
1215
+ setReconnectAttempt(0);
1162
1216
  });
1163
1217
 
1164
1218
  ws.addEventListener("message", (msgEvent) => {
@@ -1274,9 +1328,21 @@ function MonitorScreen({
1274
1328
  }
1275
1329
  });
1276
1330
 
1277
- ws.addEventListener("close", () => {
1331
+ ws.addEventListener("close", (event) => {
1278
1332
  setConnected(false);
1279
1333
  setWsStatus("disconnected");
1334
+
1335
+ // Don't reconnect if intentionally closed (code 1000) or component unmounting
1336
+ if (event.code === 1000) {
1337
+ return;
1338
+ }
1339
+
1340
+ const delay = Math.min(1000 * 2 ** reconnectAttempt, 30_000);
1341
+ setWsStatus(`reconnecting in ${Math.round(delay / 1000)}s...`);
1342
+
1343
+ reconnectTimeoutRef.current = setTimeout(() => {
1344
+ setReconnectAttempt((prev) => prev + 1);
1345
+ }, delay);
1280
1346
  });
1281
1347
 
1282
1348
  ws.addEventListener("error", () => {
@@ -1284,9 +1350,12 @@ function MonitorScreen({
1284
1350
  });
1285
1351
 
1286
1352
  return () => {
1287
- ws.close();
1353
+ if (reconnectTimeoutRef.current) {
1354
+ clearTimeout(reconnectTimeoutRef.current);
1355
+ }
1356
+ ws.close(1000); // Intentional close — code 1000 prevents reconnect
1288
1357
  };
1289
- }, [ep.slug, mach.id, mach.name, mach.forwardUrl]);
1358
+ }, [ep.slug, mach.id, mach.name, mach.forwardUrl, reconnectAttempt]);
1290
1359
 
1291
1360
  useKeyboard((key) => {
1292
1361
  if (key.name === "q" || (key.ctrl && key.name === "c")) {