@cvasingh/httpsurl 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,98 +1,158 @@
1
- # @httpsurl/client
1
+ # @cvasingh/httpsurl
2
2
 
3
- CLI tool to expose local HTTP servers to the internet via httpsurl.in tunnels.
3
+ CLI tool to expose local HTTP servers to the internet via secure HTTPS tunnels at `httpsurl.in`.
4
+
5
+ Published on npm as [`@cvasingh/httpsurl`](https://www.npmjs.com/package/@cvasingh/httpsurl).
4
6
 
5
7
  ## Installation
6
8
 
7
9
  ```bash
8
- # From the monorepo root
9
- npm install
10
+ npm install -g @cvasingh/httpsurl
11
+ ```
12
+
13
+ Both `hsurl` and `httpsurl` commands are available after install.
14
+
15
+ ## Command Reference
16
+
17
+ | Command | Aliases | Description |
18
+ |---------|---------|-------------|
19
+ | `hsurl -v` | `-V`, `--version` | Show version |
20
+ | `hsurl -h` | `--help` | Show help |
21
+ | `hsurl login` | `signin`, `auth` | Authenticate via browser |
22
+ | `hsurl login --token <t>` | | Set token directly |
23
+ | `hsurl logout` | `signout` | Remove saved credentials |
24
+ | `hsurl http <port>` | `live`, `start`, `tunnel`, `expose`, `serve` | Create a tunnel |
25
+ | `hsurl http <port> -s <url>` | | Custom server URL |
26
+ | `hsurl status` | `info` | Show authentication status |
27
+ | `hsurl whoami` | `me` | Print current user ID |
28
+
29
+ ## Quick Start
30
+
31
+ ```bash
32
+ # 1. Login
33
+ hsurl login
10
34
 
11
- # Or install globally (after publishing)
12
- npm install -g @httpsurl/client
35
+ # 2. Start a local server
36
+ npx http-server -p 8080
37
+
38
+ # 3. Expose it
39
+ hsurl live 8080
13
40
  ```
14
41
 
42
+ Your server is now accessible at `https://<tunnelId>.httpsurl.in`.
43
+
15
44
  ## Usage
16
45
 
17
46
  ### Login
18
47
 
19
48
  ```bash
20
- # Opens browser for Clerk authentication
21
- httpsurl login
49
+ # Opens browser for Clerk authentication + CLI token exchange
50
+ hsurl login
22
51
 
23
- # Or provide token directly
24
- httpsurl login --token <clerk-session-token>
52
+ # Or provide a CLI token directly
53
+ hsurl login --token <cli-token>
54
+
55
+ # Aliases
56
+ hsurl signin
57
+ hsurl auth
25
58
  ```
26
59
 
27
- Token is stored in `~/.httpsurl/credentials.json` (chmod 600).
60
+ The login flow:
61
+ 1. CLI starts a temporary localhost HTTP server
62
+ 2. Opens browser to `https://httpsurl.in/cli-auth?callback=http://localhost:PORT`
63
+ 3. You sign in via Clerk in the browser
64
+ 4. The browser exchanges the Clerk JWT for a long-lived CLI token via `POST /api/cli-token`
65
+ 5. The CLI token (`cli_<userId>_<hash>`) is redirected back to the CLI callback
66
+ 6. Token is saved to `~/.httpsurl/credentials.json` (chmod 600)
28
67
 
29
68
  ### Create a Tunnel
30
69
 
31
70
  ```bash
32
71
  # Expose local port 3000
33
- httpsurl http 3000
34
-
35
- # Custom server URL
36
- httpsurl http 8080 --server wss://httpsurl.in/connect
37
-
38
- # Local development
39
- httpsurl http 8080 --server ws://localhost:3000/connect
72
+ hsurl http 3000
73
+ hsurl live 3000
74
+ hsurl start 3000
75
+ hsurl tunnel 3000
76
+ hsurl expose 3000
77
+ hsurl serve 3000
78
+
79
+ # Custom server URL (for local development)
80
+ hsurl http 8080 --server ws://localhost:3000/connect
40
81
  ```
41
82
 
42
83
  Output:
43
84
 
44
85
  ```
45
- Connecting to wss://httpsurl.in/connect...
46
- Connected, registering tunnel...
47
- Tunnel registered!
86
+ i Connecting to wss://httpsurl.in/connect...
87
+ i Connected, registering tunnel...
88
+ v Tunnel registered!
48
89
 
49
90
  httpsurl.in
50
- ─────────────────────────────
51
- Forwarding: http://abc123def.httpsurl.in
52
- ─────────────────────────────
91
+ -----------------------------
92
+ > Forwarding: https://abc123def456.httpsurl.in
93
+ -----------------------------
94
+
95
+ > GET / 200 12ms
96
+ > POST /api/data 201 45ms
97
+ > GET /style.css 200 3ms
98
+ ```
99
+
100
+ ### Account
101
+
102
+ ```bash
103
+ # Check auth status
104
+ hsurl status
105
+ hsurl info
106
+
107
+ # Print user ID
108
+ hsurl whoami
109
+ hsurl me
53
110
 
54
- GET / 200 12ms
55
- POST /api/data 201 45ms
56
- GET /style.css 200 3ms
111
+ # Logout
112
+ hsurl logout
113
+ hsurl signout
57
114
  ```
58
115
 
59
116
  ## Modules
60
117
 
61
- ### `cli.js` Commander CLI
62
- Two commands:
63
- - `httpsurl login` Opens browser to Clerk sign-in, receives token via localhost callback server
64
- - `httpsurl http <port>` Creates tunnel with `--server` option (default: `wss://httpsurl.in/connect`)
118
+ ### `cli.js` -- Commander CLI
119
+ Commands with aliases:
120
+ - `login` (`signin`, `auth`) -- Opens browser to Clerk sign-in, exchanges JWT for CLI token
121
+ - `http` (`live`, `start`, `tunnel`, `expose`, `serve`) -- Creates tunnel with `--server` option
122
+ - `status` (`info`) -- Shows authentication status and credentials path
123
+ - `whoami` (`me`) -- Prints current user ID
124
+ - `logout` (`signout`) -- Removes saved credentials
65
125
 
66
- ### `tunnel-client.js` WebSocket Client
67
- - Connects to server, sends `register` message with Clerk token
126
+ ### `tunnel-client.js` -- WebSocket Client
127
+ - Connects to server, sends `register` message with CLI token
68
128
  - Receives `registered` with tunnel URL
69
129
  - Handles `request` messages by proxying to local server via `local-proxy.js`
70
130
  - Sends `response` or `response_error` back
71
131
  - Auto-reconnects with exponential backoff on disconnect
72
- - Preserves tunnel ID across reconnections
132
+ - Preserves tunnel ID across reconnections (within 60s grace period)
73
133
 
74
- ### `local-proxy.js` HTTP Proxy
134
+ ### `local-proxy.js` -- HTTP Proxy
75
135
  - Makes HTTP requests to `127.0.0.1:<port>`
76
136
  - Strips `host` and `x-tunnel-host` headers
77
137
  - Base64 encodes/decodes request and response bodies
78
138
  - 30s timeout per request
79
139
 
80
- ### `reconnect.js` Exponential Backoff
140
+ ### `reconnect.js` -- Exponential Backoff
81
141
  - Initial delay: 1s
82
- - Doubles each attempt: 1s 2s 4s 8s 16s 30s (max)
142
+ - Doubles each attempt: 1s -> 2s -> 4s -> 8s -> 16s -> 30s (max)
83
143
  - Resets on successful reconnection
84
144
  - Cancellable
85
145
 
86
- ### `logger.js` Terminal Output
146
+ ### `logger.js` -- Terminal Output
87
147
  Colored output using chalk:
88
- - `info` blue
89
- - `success` green
90
- - `warn` yellow
91
- - `error` red
92
- - `request` method-colored badges with status code and latency
93
- - `banner` formatted tunnel URL display
94
-
95
- ### `protocol.js` Message Constants
148
+ - `info` -- blue
149
+ - `success` -- green
150
+ - `warn` -- yellow
151
+ - `error` -- red
152
+ - `request` -- method-colored badges with status code and latency
153
+ - `banner` -- formatted tunnel URL display
154
+
155
+ ### `protocol.js` -- Message Constants
96
156
  Shared message type constants (duplicated from server for simplicity):
97
157
  - `MSG_REGISTER`, `MSG_RESPONSE`, `MSG_RESPONSE_ERROR`
98
158
  - `MSG_REGISTERED`, `MSG_REQUEST`, `MSG_ERROR`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cvasingh/httpsurl",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Expose local servers to the internet via httpsurl.in tunnels",
5
5
  "keywords": [
6
6
  "tunnel",
@@ -18,6 +18,7 @@
18
18
  },
19
19
  "author": "cvasingh",
20
20
  "bin": {
21
+ "hsurl": "bin/httpsurl.js",
21
22
  "httpsurl": "bin/httpsurl.js"
22
23
  },
23
24
  "files": [
package/src/cli.js CHANGED
@@ -1,16 +1,16 @@
1
- const { Command } = require('commander');
2
- const { TunnelClient } = require('./tunnel-client');
3
- const log = require('./logger');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
1
+ const { Command } = require("commander");
2
+ const { TunnelClient } = require("./tunnel-client");
3
+ const log = require("./logger");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const os = require("os");
7
7
 
8
- const CREDENTIALS_DIR = path.join(os.homedir(), '.httpsurl');
9
- const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json');
8
+ const CREDENTIALS_DIR = path.join(os.homedir(), ".httpsurl");
9
+ const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
10
10
 
11
11
  function loadToken() {
12
12
  try {
13
- const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf8'));
13
+ const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf8"));
14
14
  return data.token;
15
15
  } catch {
16
16
  return null;
@@ -23,108 +23,178 @@ function saveToken(token) {
23
23
  fs.chmodSync(CREDENTIALS_FILE, 0o600);
24
24
  }
25
25
 
26
+ // Shared tunnel action
27
+ async function tunnelAction(port, options) {
28
+ if (!port || port < 1 || port > 65535) {
29
+ log.error("Invalid port number");
30
+ process.exit(1);
31
+ }
32
+
33
+ const token = loadToken();
34
+ if (!token) {
35
+ log.error("Not authenticated. Run: hsurl login");
36
+ process.exit(1);
37
+ }
38
+
39
+ const client = new TunnelClient({
40
+ serverUrl: options.server,
41
+ localPort: port,
42
+ token,
43
+ });
44
+
45
+ process.on("SIGINT", () => {
46
+ log.info("\nShutting down...");
47
+ client.close();
48
+ process.exit(0);
49
+ });
50
+
51
+ try {
52
+ await client.connect();
53
+ } catch (err) {
54
+ log.error(`Failed to connect: ${err.message}`);
55
+ process.exit(1);
56
+ }
57
+ }
58
+
59
+ // Shared login action
60
+ async function loginAction(options) {
61
+ if (options.token) {
62
+ saveToken(options.token);
63
+ log.success("Token saved!");
64
+ return;
65
+ }
66
+
67
+ const http = require("http");
68
+
69
+ const server = http.createServer((req, res) => {
70
+ const url = new URL(req.url, "http://localhost");
71
+ const token = url.searchParams.get("token");
72
+
73
+ if (token) {
74
+ saveToken(token);
75
+ res.writeHead(200, { "Content-Type": "text/html" });
76
+ res.end(`
77
+ <html>
78
+ <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
79
+ <div style="text-align: center;">
80
+ <h1 style="font-weight: 300;">Authenticated</h1>
81
+ <p style="color: #666;">You can close this window and return to the terminal.</p>
82
+ </div>
83
+ </body>
84
+ </html>
85
+ `);
86
+ server.close();
87
+ log.success("Authenticated successfully!");
88
+ process.exit(0);
89
+ } else {
90
+ res.writeHead(400);
91
+ res.end("Missing token");
92
+ }
93
+ });
94
+
95
+ server.listen(0, async () => {
96
+ const port = server.address().port;
97
+ const callbackUrl = `http://localhost:${port}`;
98
+ const loginUrl = `https://httpsurl.in/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
99
+
100
+ log.info("Opening browser for authentication...");
101
+ log.info(`If browser doesn't open, visit: ${loginUrl}`);
102
+
103
+ try {
104
+ const open = require("open");
105
+ await open(loginUrl);
106
+ } catch {
107
+ log.warn("Could not open browser automatically");
108
+ }
109
+ });
110
+ }
111
+
26
112
  function createCli() {
27
113
  const program = new Command();
28
114
 
29
115
  program
30
- .name('httpsurl')
31
- .description('Expose local servers to the internet via httpsurl.in')
32
- .version('1.0.0');
116
+ .name("hsurl")
117
+ .description("Expose local servers to the internet via httpsurl.in")
118
+ .version("1.1.0", "-v, -V, --version");
33
119
 
34
- // Login command
120
+ // ── login / signin / auth ──
121
+ program
122
+ .command("login")
123
+ .alias("signin")
124
+ .alias("auth")
125
+ .description("Authenticate with httpsurl.in")
126
+ .option("--token <token>", "Provide token directly")
127
+ .action(loginAction);
128
+
129
+ // ── logout / signout ──
35
130
  program
36
- .command('login')
37
- .description('Authenticate with httpsurl.in')
38
- .option('--token <token>', 'Provide token directly')
39
- .action(async (options) => {
40
- if (options.token) {
41
- saveToken(options.token);
42
- log.success('Token saved!');
43
- return;
131
+ .command("logout")
132
+ .alias("signout")
133
+ .description("Remove saved credentials")
134
+ .action(() => {
135
+ try {
136
+ fs.unlinkSync(CREDENTIALS_FILE);
137
+ log.success("Logged out successfully");
138
+ } catch {
139
+ log.info("No credentials found");
44
140
  }
45
-
46
- // Open browser for Clerk auth
47
- const http = require('http');
48
-
49
- const server = http.createServer((req, res) => {
50
- const url = new URL(req.url, 'http://localhost');
51
- const token = url.searchParams.get('token');
52
-
53
- if (token) {
54
- saveToken(token);
55
- res.writeHead(200, { 'Content-Type': 'text/html' });
56
- res.end(`
57
- <html>
58
- <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
59
- <div style="text-align: center;">
60
- <h1 style="font-weight: 300;">✓ Authenticated</h1>
61
- <p style="color: #666;">You can close this window and return to the terminal.</p>
62
- </div>
63
- </body>
64
- </html>
65
- `);
66
- server.close();
67
- log.success('Authenticated successfully!');
68
- process.exit(0);
69
- } else {
70
- res.writeHead(400);
71
- res.end('Missing token');
72
- }
73
- });
74
-
75
- server.listen(0, async () => {
76
- const port = server.address().port;
77
- const callbackUrl = `http://localhost:${port}`;
78
- const loginUrl = `https://httpsurl.in/sign-in?redirect_url=${encodeURIComponent(callbackUrl)}`;
79
-
80
- log.info(`Opening browser for authentication...`);
81
- log.info(`If browser doesn't open, visit: ${loginUrl}`);
82
-
83
- try {
84
- const open = require('open');
85
- await open(loginUrl);
86
- } catch {
87
- log.warn('Could not open browser automatically');
88
- }
89
- });
90
141
  });
91
142
 
92
- // HTTP tunnel command
143
+ // ── http / live / start / tunnel / expose / serve ──
144
+ const tunnelOpts = [
145
+ "-s, --server <url>",
146
+ "Server WebSocket URL",
147
+ "wss://httpsurl.in/connect",
148
+ ];
149
+
150
+ program
151
+ .command("http")
152
+ .alias("live")
153
+ .alias("start")
154
+ .alias("tunnel")
155
+ .alias("expose")
156
+ .alias("serve")
157
+ .description("Create a tunnel to a local HTTP server")
158
+ .argument("<port>", "Local port to expose", parseInt)
159
+ .option(...tunnelOpts)
160
+ .action(tunnelAction);
161
+
162
+ // ── status / info ──
93
163
  program
94
- .command('http')
95
- .description('Create a tunnel to a local HTTP server')
96
- .argument('<port>', 'Local port to expose', parseInt)
97
- .option('-s, --server <url>', 'Server WebSocket URL', 'wss://httpsurl.in/connect')
98
- .action(async (port, options) => {
99
- if (!port || port < 1 || port > 65535) {
100
- log.error('Invalid port number');
164
+ .command("status")
165
+ .alias("info")
166
+ .description("Show current authentication status")
167
+ .action(() => {
168
+ const token = loadToken();
169
+ if (!token) {
170
+ log.error("Not authenticated. Run: hsurl login");
101
171
  process.exit(1);
102
172
  }
173
+ if (token.startsWith("cli_")) {
174
+ const userId = token.split("_").slice(1, -1).join("_");
175
+ log.success(`Authenticated as ${userId}`);
176
+ } else {
177
+ log.success("Authenticated (token saved)");
178
+ }
179
+ log.info(`Credentials: ${CREDENTIALS_FILE}`);
180
+ });
103
181
 
182
+ // ── whoami / me ──
183
+ program
184
+ .command("whoami")
185
+ .alias("me")
186
+ .description("Show the current authenticated user")
187
+ .action(() => {
104
188
  const token = loadToken();
105
189
  if (!token) {
106
- log.error('Not authenticated. Run: httpsurl login');
190
+ log.error("Not authenticated. Run: hsurl login");
107
191
  process.exit(1);
108
192
  }
109
-
110
- const client = new TunnelClient({
111
- serverUrl: options.server,
112
- localPort: port,
113
- token,
114
- });
115
-
116
- // Graceful shutdown
117
- process.on('SIGINT', () => {
118
- log.info('\nShutting down...');
119
- client.close();
120
- process.exit(0);
121
- });
122
-
123
- try {
124
- await client.connect();
125
- } catch (err) {
126
- log.error(`Failed to connect: ${err.message}`);
127
- process.exit(1);
193
+ if (token.startsWith("cli_")) {
194
+ const userId = token.split("_").slice(1, -1).join("_");
195
+ console.log(userId);
196
+ } else {
197
+ console.log("authenticated (legacy token)");
128
198
  }
129
199
  });
130
200
 
@@ -1,8 +1,15 @@
1
- const WebSocket = require('ws');
2
- const { MSG_REGISTER, MSG_REGISTERED, MSG_REQUEST, MSG_RESPONSE, MSG_RESPONSE_ERROR, MSG_ERROR } = require('./protocol');
3
- const { makeLocalRequest } = require('./local-proxy');
4
- const { ReconnectStrategy } = require('./reconnect');
5
- const log = require('./logger');
1
+ const WebSocket = require("ws");
2
+ const {
3
+ MSG_REGISTER,
4
+ MSG_REGISTERED,
5
+ MSG_REQUEST,
6
+ MSG_RESPONSE,
7
+ MSG_RESPONSE_ERROR,
8
+ MSG_ERROR,
9
+ } = require("./protocol");
10
+ const { makeLocalRequest } = require("./local-proxy");
11
+ const { ReconnectStrategy } = require("./reconnect");
12
+ const log = require("./logger");
6
13
 
7
14
  class TunnelClient {
8
15
  constructor(options) {
@@ -26,31 +33,33 @@ class TunnelClient {
26
33
 
27
34
  this.ws = new WebSocket(url);
28
35
 
29
- this.ws.on('open', () => {
30
- log.info('Connected, registering tunnel...');
36
+ this.ws.on("open", () => {
37
+ log.info("Connected, registering tunnel...");
31
38
 
32
39
  // Send register message
33
- this.ws.send(JSON.stringify({
34
- type: MSG_REGISTER,
35
- token: this.token,
36
- port: this.localPort,
37
- tunnelId: this.tunnelId, // null on first connect, set on reconnect
38
- }));
40
+ this.ws.send(
41
+ JSON.stringify({
42
+ type: MSG_REGISTER,
43
+ token: this.token,
44
+ port: this.localPort,
45
+ tunnelId: this.tunnelId, // null on first connect, set on reconnect
46
+ }),
47
+ );
39
48
  });
40
49
 
41
- this.ws.on('message', (data) => {
50
+ this.ws.on("message", (data) => {
42
51
  let msg;
43
52
  try {
44
53
  msg = JSON.parse(data.toString());
45
54
  } catch {
46
- log.error('Invalid message from server');
55
+ log.error("Invalid message from server");
47
56
  return;
48
57
  }
49
58
 
50
59
  this._handleMessage(msg, resolve);
51
60
  });
52
61
 
53
- this.ws.on('close', (code, reason) => {
62
+ this.ws.on("close", (code, reason) => {
54
63
  if (this._closing) return;
55
64
 
56
65
  log.warn(`Disconnected (code: ${code}). Reconnecting...`);
@@ -58,7 +67,7 @@ class TunnelClient {
58
67
  log.info(`Reconnecting in ${delay}ms...`);
59
68
  });
60
69
 
61
- this.ws.on('error', (err) => {
70
+ this.ws.on("error", (err) => {
62
71
  if (this._closing) return;
63
72
 
64
73
  // Only reject on first connection attempt
@@ -78,7 +87,7 @@ class TunnelClient {
78
87
  this._reconnect.reset();
79
88
 
80
89
  log.success(`Tunnel registered!`);
81
- log.banner(`http://${msg.url}`);
90
+ log.banner(`https://${msg.url}`);
82
91
 
83
92
  if (resolveConnect) resolveConnect(this);
84
93
  break;
@@ -109,22 +118,26 @@ class TunnelClient {
109
118
  body,
110
119
  });
111
120
 
112
- this.ws.send(JSON.stringify({
113
- type: MSG_RESPONSE,
114
- requestId,
115
- statusCode: response.statusCode,
116
- headers: response.headers,
117
- body: response.body,
118
- }));
121
+ this.ws.send(
122
+ JSON.stringify({
123
+ type: MSG_RESPONSE,
124
+ requestId,
125
+ statusCode: response.statusCode,
126
+ headers: response.headers,
127
+ body: response.body,
128
+ }),
129
+ );
119
130
 
120
131
  const latency = Date.now() - start;
121
132
  log.request(method, path, response.statusCode, latency);
122
133
  } catch (err) {
123
- this.ws.send(JSON.stringify({
124
- type: MSG_RESPONSE_ERROR,
125
- requestId,
126
- error: err.message,
127
- }));
134
+ this.ws.send(
135
+ JSON.stringify({
136
+ type: MSG_RESPONSE_ERROR,
137
+ requestId,
138
+ error: err.message,
139
+ }),
140
+ );
128
141
 
129
142
  log.error(`${method} ${path} → Error: ${err.message}`);
130
143
  }
@@ -134,7 +147,7 @@ class TunnelClient {
134
147
  this._closing = true;
135
148
  this._reconnect.destroy();
136
149
  if (this.ws) {
137
- this.ws.close(1000, 'Client closing');
150
+ this.ws.close(1000, "Client closing");
138
151
  }
139
152
  }
140
153