@zintrust/expose 0.4.58
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 +79 -0
- package/dist/ExposeCommand.d.ts +5 -0
- package/dist/ExposeCommand.d.ts.map +1 -0
- package/dist/ExposeCommand.js +48 -0
- package/dist/TunnelManager.d.ts +2 -0
- package/dist/TunnelManager.d.ts.map +1 -0
- package/dist/TunnelManager.js +13 -0
- package/dist/build-manifest.json +114 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/providers/CloudflareProvider.d.ts +4 -0
- package/dist/providers/CloudflareProvider.d.ts.map +1 -0
- package/dist/providers/CloudflareProvider.js +98 -0
- package/dist/providers/ITunnelProvider.d.ts +16 -0
- package/dist/providers/ITunnelProvider.d.ts.map +1 -0
- package/dist/providers/ITunnelProvider.js +1 -0
- package/dist/providers/ZintrustProvider.d.ts +5 -0
- package/dist/providers/ZintrustProvider.d.ts.map +1 -0
- package/dist/providers/ZintrustProvider.js +13 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +11 -0
- package/package.json +37 -0
- package/src/ExposeCommand.ts +56 -0
- package/src/TunnelManager.ts +15 -0
- package/src/index.ts +3 -0
- package/src/providers/CloudflareProvider.ts +132 -0
- package/src/providers/ITunnelProvider.ts +18 -0
- package/src/providers/ZintrustProvider.ts +18 -0
- package/src/register.ts +20 -0
- package/tsconfig.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @zintrust/expose
|
|
2
|
+
|
|
3
|
+
The `@zintrust/expose` package provides an official ZinTrust CLI extension to instantly expose your local development environment to the internet via secure tunnels.
|
|
4
|
+
|
|
5
|
+
It allows you to share your work with clients, test webhooks, or test on mobile devices without deploying your application.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
This package requires a ZinTrust project using `@zintrust/core` `^0.4.0` or higher to run. By default, standard templates include and load this package automatically.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Use the global `zin` command (or `npx tsx bin/zin.ts`) in your application root to invoke the `expose` CLI command (also aliased as `exp`).
|
|
14
|
+
|
|
15
|
+
### Basic Command
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
zin expose [port] [options]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Options
|
|
22
|
+
|
|
23
|
+
- `[port]`: The local port you want to expose. If omitted, ZinTrust will attempt to read `process.env.PORT` from your `.env` file, defaulting to `3000` otherwise.
|
|
24
|
+
- `--provider <provider>`: The backend tunneling service to use. Supports `cloudflare` or `zintrust`. (Default: `cloudflare`)
|
|
25
|
+
- `--https`: Enable if the local target you are exposing expects HTTPS traffic (e.g., exposing a local self-signed dev server).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Tunnel Providers
|
|
30
|
+
|
|
31
|
+
The package ships with two native tunneling providers:
|
|
32
|
+
|
|
33
|
+
### 1. Cloudflare Tunnels (Default)
|
|
34
|
+
|
|
35
|
+
The `cloudflare` provider utilizes Cloudflare's `cloudflared` utility behind the scenes to spawn rapid, ephemeral tunnels. It's incredibly fast and requires no authentication, granting you a randomized `.trycloudflare.com` URL instantly.
|
|
36
|
+
|
|
37
|
+
**Examples:**
|
|
38
|
+
|
|
39
|
+
Expose the default port (automatically read from `.env`):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
zin exp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Explicitly expose port `8080` via Cloudflare:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
zin exp 8080 --provider cloudflare
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Expose a local HTTPS container running on port `443`:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
zin exp 443 --https
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. ZinTrust Tunnels
|
|
58
|
+
|
|
59
|
+
The `zintrust` provider connects to the ZinTrust internal tunneling network. This is useful for authenticated developer environments, persistent subdomains, and connecting services inside the ZinTrust ecosystem.
|
|
60
|
+
|
|
61
|
+
**Examples:**
|
|
62
|
+
|
|
63
|
+
Expose port `3000` using the native ZinTrust tunnel:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
zin expose 3000 --provider zintrust
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Expose your local environment via ZinTrust securely over HTTPS:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
zin exp --https --provider zintrust
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## How It Works Under The Hood
|
|
76
|
+
|
|
77
|
+
ZinTrust registers `@zintrust/expose` as an _Optional CLI Extension_. When the framework bootstraps `bin/zin.ts`, the package exposes its provider definitions `ITunnelProvider` through `TunnelManager`.
|
|
78
|
+
|
|
79
|
+
When a tunnel is requested, a child process hooks into the requested provider (like `cloudflared`), safely negotiating the handshake and parsing the generated secure URL back to the developer console. When the CLI is exited (via `Ctrl+C`), the package safely and automatically cleans up the tunnel child processes.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExposeCommand.d.ts","sourceRoot":"","sources":["../src/ExposeCommand.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmDpC,eAAO,MAAM,aAAa;sBA3CM,OAAO;;EA8CrC,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import * as dotenv from 'dotenv';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { createTunnelProvider } from './TunnelManager.js';
|
|
6
|
+
// Since this CLI runs early, we parse .env for the default PORT if unspecified
|
|
7
|
+
dotenv.config({ path: resolve(process.cwd(), '.env') });
|
|
8
|
+
const createExposeCommand = () => {
|
|
9
|
+
const command = new Command('expose')
|
|
10
|
+
.alias('exp')
|
|
11
|
+
.description('Expose your local ZinTrust server to the internet using secure tunnels.')
|
|
12
|
+
.argument('[port]', 'The local port to expose', process.env['PORT'] || '3000')
|
|
13
|
+
.option('--https', 'Connect locally via HTTPS instead of HTTP', false)
|
|
14
|
+
.option('--provider <provider>', 'Tunnel provider (zintrust | cloudflare)', 'cloudflare')
|
|
15
|
+
.action(async (portArg, options) => {
|
|
16
|
+
const port = Number.parseInt(portArg, 10);
|
|
17
|
+
if (Number.isNaN(port)) {
|
|
18
|
+
console.error(`Invalid port provided: ${portArg}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
console.log(`Starting URL exposure on port ${port} using ${options.provider} provider...`);
|
|
22
|
+
const provider = createTunnelProvider(options.provider);
|
|
23
|
+
try {
|
|
24
|
+
const publicUrl = await provider.start({ port, https: options.https });
|
|
25
|
+
console.log(`\n=============================================================`);
|
|
26
|
+
console.log(`🚀 Tunnel Established successfully!`);
|
|
27
|
+
console.log(`🌍 Public URL : \x1b[32m${publicUrl}\x1b[0m`);
|
|
28
|
+
console.log(`🔌 Local Target : ${options.https ? 'https' : 'http'}://localhost:${port}`);
|
|
29
|
+
console.log(`🛡️ Provider : ${options.provider}`);
|
|
30
|
+
console.log(`=============================================================\n`);
|
|
31
|
+
console.log(`Press Ctrl+C to disconnect from the tunnel.\n`);
|
|
32
|
+
process.on('SIGINT', async () => {
|
|
33
|
+
console.log(`\nDisconnecting ${options.provider} tunnel...`);
|
|
34
|
+
await provider.stop();
|
|
35
|
+
process.exit(0);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error(`Tunnel failed to start:`, error.message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return command;
|
|
44
|
+
};
|
|
45
|
+
export const ExposeCommand = Object.freeze({
|
|
46
|
+
getCommand: createExposeCommand,
|
|
47
|
+
name: 'expose',
|
|
48
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TunnelManager.d.ts","sourceRoot":"","sources":["../src/TunnelManager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAGtE,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,eAAe,CAU1E"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CloudflareProvider } from './providers/CloudflareProvider.js';
|
|
2
|
+
import { ZintrustProvider } from './providers/ZintrustProvider.js';
|
|
3
|
+
export function createTunnelProvider(providerName) {
|
|
4
|
+
switch (providerName.toLowerCase()) {
|
|
5
|
+
case 'cloudflare':
|
|
6
|
+
case 'cf':
|
|
7
|
+
return CloudflareProvider.create();
|
|
8
|
+
case 'zintrust':
|
|
9
|
+
case 'zin':
|
|
10
|
+
default:
|
|
11
|
+
return new ZintrustProvider();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zintrust/expose",
|
|
3
|
+
"version": "0.4.58",
|
|
4
|
+
"buildDate": "2026-04-04T19:58:21.241Z",
|
|
5
|
+
"buildEnvironment": {
|
|
6
|
+
"node": "v22.22.1",
|
|
7
|
+
"platform": "darwin",
|
|
8
|
+
"arch": "arm64"
|
|
9
|
+
},
|
|
10
|
+
"git": {
|
|
11
|
+
"commit": "99e4d331",
|
|
12
|
+
"branch": "release"
|
|
13
|
+
},
|
|
14
|
+
"package": {
|
|
15
|
+
"dependencies": [
|
|
16
|
+
"cloudflared",
|
|
17
|
+
"commander",
|
|
18
|
+
"dotenv"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": [
|
|
21
|
+
"@zintrust/core"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"files": {
|
|
25
|
+
".tsbuildinfo": {
|
|
26
|
+
"size": 237,
|
|
27
|
+
"sha256": "1bbf649a4aaa9638d528f72043ee08eed6f83ea8dc3b1ae79a8eecd6951c33c8"
|
|
28
|
+
},
|
|
29
|
+
"ExposeCommand.d.ts": {
|
|
30
|
+
"size": 139,
|
|
31
|
+
"sha256": "1d626d9af74c580209e2a25698b988df26f3618de86fe25d5f210571ae17be4e"
|
|
32
|
+
},
|
|
33
|
+
"ExposeCommand.d.ts.map": {
|
|
34
|
+
"size": 206,
|
|
35
|
+
"sha256": "27216831474eb9679ca05e762c431d11df8bfe27c4cf25d62ea46dd066f1600b"
|
|
36
|
+
},
|
|
37
|
+
"ExposeCommand.js": {
|
|
38
|
+
"size": 2366,
|
|
39
|
+
"sha256": "f03dfe8cf14ae1b23f066a8e3e7b7d361667c7e915213acc141cf76e81834652"
|
|
40
|
+
},
|
|
41
|
+
"TunnelManager.d.ts": {
|
|
42
|
+
"size": 156,
|
|
43
|
+
"sha256": "c124af9323f0215aa3e4c511d88541e53cab0836e9dd570d9d05b84f913bcf81"
|
|
44
|
+
},
|
|
45
|
+
"TunnelManager.d.ts.map": {
|
|
46
|
+
"size": 222,
|
|
47
|
+
"sha256": "70ab4e635b2b7e38423a7d30a2d9e73e84444dae6e76ddf480dd90e697f3a378"
|
|
48
|
+
},
|
|
49
|
+
"TunnelManager.js": {
|
|
50
|
+
"size": 442,
|
|
51
|
+
"sha256": "16cedc84ff7d7575514eede3c86d022e84e529bebc32147a2cc0097ec80f25ed"
|
|
52
|
+
},
|
|
53
|
+
"index.d.ts": {
|
|
54
|
+
"size": 118,
|
|
55
|
+
"sha256": "29e1ca420de1f7e4b0325e3b555ee9cca48f8eb19a15a63c9fbda99f7064cbe8"
|
|
56
|
+
},
|
|
57
|
+
"index.d.ts.map": {
|
|
58
|
+
"size": 188,
|
|
59
|
+
"sha256": "2114823c51f672d2b1d66fc1854dfaddb4857183fd90c2c0d7b3bb12e49b36f3"
|
|
60
|
+
},
|
|
61
|
+
"index.js": {
|
|
62
|
+
"size": 236,
|
|
63
|
+
"sha256": "deb8aef1bda0832afe5bbf22b21d1f1ecfddc02f97894803a96092f1e0a9567f"
|
|
64
|
+
},
|
|
65
|
+
"providers/CloudflareProvider.d.ts": {
|
|
66
|
+
"size": 152,
|
|
67
|
+
"sha256": "8cbbb4a3428cc8e030c9b280e962498c7658396fd0f4ab60d6dcff48cceb3d76"
|
|
68
|
+
},
|
|
69
|
+
"providers/CloudflareProvider.d.ts.map": {
|
|
70
|
+
"size": 238,
|
|
71
|
+
"sha256": "1c01a489040b9f251d7275ad8eb78184237e464b7973f2a5c1dd7f056a81d565"
|
|
72
|
+
},
|
|
73
|
+
"providers/CloudflareProvider.js": {
|
|
74
|
+
"size": 3335,
|
|
75
|
+
"sha256": "2032185d45cbf49006d3100a39608720ab0dc3179637f0ed3947005ed4f8be2b"
|
|
76
|
+
},
|
|
77
|
+
"providers/ITunnelProvider.d.ts": {
|
|
78
|
+
"size": 367,
|
|
79
|
+
"sha256": "d7905c566db2f564ef246dfee65791012adfc11ee983593e05fe2541e16cb828"
|
|
80
|
+
},
|
|
81
|
+
"providers/ITunnelProvider.d.ts.map": {
|
|
82
|
+
"size": 449,
|
|
83
|
+
"sha256": "d4cb56ac5d4c8931309577b14817f0ca7cb5a6a24d524c50bf9450b6f96ac2a5"
|
|
84
|
+
},
|
|
85
|
+
"providers/ITunnelProvider.js": {
|
|
86
|
+
"size": 11,
|
|
87
|
+
"sha256": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"
|
|
88
|
+
},
|
|
89
|
+
"providers/ZintrustProvider.d.ts": {
|
|
90
|
+
"size": 225,
|
|
91
|
+
"sha256": "a99419f227a6bcd040f20c260c0f3578fc5056ae4fbf0b7681c18bc12f5f7d5b"
|
|
92
|
+
},
|
|
93
|
+
"providers/ZintrustProvider.d.ts.map": {
|
|
94
|
+
"size": 322,
|
|
95
|
+
"sha256": "6f58e3adf0e9665bc83df3227e94217127cdb0865782a59f11666db788243af9"
|
|
96
|
+
},
|
|
97
|
+
"providers/ZintrustProvider.js": {
|
|
98
|
+
"size": 590,
|
|
99
|
+
"sha256": "004335170e84ab2525e98e7a25f7576d6466e0d34f268d75b6f5a078356a5f2a"
|
|
100
|
+
},
|
|
101
|
+
"register.d.ts": {
|
|
102
|
+
"size": 11,
|
|
103
|
+
"sha256": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"
|
|
104
|
+
},
|
|
105
|
+
"register.d.ts.map": {
|
|
106
|
+
"size": 110,
|
|
107
|
+
"sha256": "43d6ff274ab1483756d47f84bbbd565e33570698ffcba03ace354bebd2c37a1a"
|
|
108
|
+
},
|
|
109
|
+
"register.js": {
|
|
110
|
+
"size": 420,
|
|
111
|
+
"sha256": "63cb414c1038e014d5ff0c3b429ee2201573ba78673db725e5f72544c66f1530"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CloudflareProvider.d.ts","sourceRoot":"","sources":["../../src/providers/CloudflareProvider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,sBAAsB,CAAC;AAgI3E,eAAO,MAAM,kBAAkB;kBA1EZ,eAAe;EA4EhC,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
const createTunnelUrlHandler = (urlRegex, resolveOnce) => {
|
|
3
|
+
return (data) => {
|
|
4
|
+
const match = urlRegex.exec(data.toString());
|
|
5
|
+
if (match !== null) {
|
|
6
|
+
resolveOnce(match[1]);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
const createTunnelCloseHandler = (clearStartupTimer, shouldReject, rejectOnce) => {
|
|
11
|
+
return (code) => {
|
|
12
|
+
clearStartupTimer();
|
|
13
|
+
if (shouldReject() && code !== 0) {
|
|
14
|
+
rejectOnce(new Error(`Tunnel closed unexpectedly with code ${String(code)}`));
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
const createTunnelErrorHandler = (rejectOnce) => {
|
|
19
|
+
return (error) => {
|
|
20
|
+
rejectOnce(error);
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
const rejectStartupTimeout = (rejectOnce) => {
|
|
24
|
+
rejectOnce(new Error('Cloudflared tunnel startup timed out.'));
|
|
25
|
+
};
|
|
26
|
+
const stopAfterStartupTimeout = async (stop, rejectOnce) => {
|
|
27
|
+
try {
|
|
28
|
+
await stop();
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
rejectStartupTimeout(rejectOnce);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const createStartupTimeoutHandler = (stop, rejectOnce) => {
|
|
35
|
+
return () => {
|
|
36
|
+
void stopAfterStartupTimeout(stop, rejectOnce);
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
const create = () => {
|
|
40
|
+
let childProcess = null;
|
|
41
|
+
let isStopping = false;
|
|
42
|
+
let startupTimer;
|
|
43
|
+
const clearStartupTimer = () => {
|
|
44
|
+
if (startupTimer !== undefined) {
|
|
45
|
+
clearTimeout(startupTimer);
|
|
46
|
+
startupTimer = undefined;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const stop = async () => {
|
|
50
|
+
isStopping = true;
|
|
51
|
+
clearStartupTimer();
|
|
52
|
+
if (childProcess !== null) {
|
|
53
|
+
childProcess.kill();
|
|
54
|
+
childProcess = null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const start = async (options) => {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const scheme = options.https ? 'https' : 'http';
|
|
60
|
+
const localUrl = `${scheme}://localhost:${options.port}`;
|
|
61
|
+
const urlRegex = /(https:\/\/[a-z0-9-]+\.trycloudflare\.com)/;
|
|
62
|
+
let settled = false;
|
|
63
|
+
const resolveOnce = (url) => {
|
|
64
|
+
if (settled)
|
|
65
|
+
return;
|
|
66
|
+
settled = true;
|
|
67
|
+
clearStartupTimer();
|
|
68
|
+
resolve(url);
|
|
69
|
+
};
|
|
70
|
+
const rejectOnce = (error) => {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
settled = true;
|
|
74
|
+
clearStartupTimer();
|
|
75
|
+
reject(error);
|
|
76
|
+
};
|
|
77
|
+
process.stdout.write(`Starting cloudflared tunnel to ${localUrl}...\n`);
|
|
78
|
+
childProcess = spawn('npx', ['cloudflared', 'tunnel', '--url', localUrl], {
|
|
79
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
|
+
});
|
|
81
|
+
const handleTunnelUrl = createTunnelUrlHandler(urlRegex, resolveOnce);
|
|
82
|
+
const handleClose = createTunnelCloseHandler(clearStartupTimer, () => !isStopping && !settled, rejectOnce);
|
|
83
|
+
const handleError = createTunnelErrorHandler(rejectOnce);
|
|
84
|
+
const handleStartupTimeout = createStartupTimeoutHandler(stop, rejectOnce);
|
|
85
|
+
childProcess.stderr?.on('data', handleTunnelUrl);
|
|
86
|
+
childProcess.on('close', handleClose);
|
|
87
|
+
childProcess.on('error', handleError);
|
|
88
|
+
startupTimer = globalThis.setTimeout(handleStartupTimeout, 15000);
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
return Object.freeze({
|
|
92
|
+
start,
|
|
93
|
+
stop,
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
export const CloudflareProvider = Object.freeze({
|
|
97
|
+
create,
|
|
98
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TunnelOptions {
|
|
2
|
+
port: number;
|
|
3
|
+
https: boolean;
|
|
4
|
+
subdomain?: string;
|
|
5
|
+
providerOptions?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface ITunnelProvider {
|
|
8
|
+
/**
|
|
9
|
+
* Start the tunnel and return the public URL.
|
|
10
|
+
*/
|
|
11
|
+
start(options: TunnelOptions): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Stop the tunnel.
|
|
14
|
+
*/
|
|
15
|
+
stop(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ITunnelProvider.d.ts","sourceRoot":"","sources":["../../src/providers/ITunnelProvider.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE/C;;OAEG;IACH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ZintrustProvider.d.ts","sourceRoot":"","sources":["../../src/providers/ZintrustProvider.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE3E,qBAAa,gBAAiB,YAAW,eAAe;IAChD,KAAK,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAS/C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAG5B"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class ZintrustProvider {
|
|
2
|
+
async start(_options) {
|
|
3
|
+
// In the future, this will connect to the official ZinTrust tunneled infrastructure
|
|
4
|
+
// via WebSockets or a similar reverse proxy technique.
|
|
5
|
+
// For now, we stub this out or fallback to locolhost.
|
|
6
|
+
console.warn('ZinTrust provider is coming soon in the expose package.');
|
|
7
|
+
console.warn('Please use `--provider cloudflare` instead for now.');
|
|
8
|
+
throw new Error('ZinTrust Tunnel Provider not implemented yet');
|
|
9
|
+
}
|
|
10
|
+
async stop() {
|
|
11
|
+
// Clean up WS connections or sockets
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":""}
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ExposeCommand } from './ExposeCommand.js';
|
|
2
|
+
try {
|
|
3
|
+
const core = (await import('@zintrust/core'));
|
|
4
|
+
if (core.OptionalCliCommandRegistry !== undefined) {
|
|
5
|
+
core.OptionalCliCommandRegistry.register('expose', ExposeCommand);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10
|
+
process.stderr.write(`Failed to register expose command: ${message}\n`);
|
|
11
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zintrust/expose",
|
|
3
|
+
"version": "0.4.58",
|
|
4
|
+
"description": "ZinTrust local tunnel exposure package",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./register": {
|
|
14
|
+
"types": "./dist/register.d.ts",
|
|
15
|
+
"default": "./dist/register.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -b",
|
|
20
|
+
"watch": "tsc -b -w",
|
|
21
|
+
"clean": "rm -rf dist"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"cloudflared": "^0.3.0",
|
|
25
|
+
"commander": "^14.0.3",
|
|
26
|
+
"dotenv": "^16.4.5"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.0.0",
|
|
30
|
+
"typescript": "^5.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@zintrust/core": "^0.4.58"
|
|
34
|
+
},
|
|
35
|
+
"author": "ZinTrust",
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import * as dotenv from 'dotenv';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { createTunnelProvider } from './TunnelManager.js';
|
|
6
|
+
|
|
7
|
+
// Since this CLI runs early, we parse .env for the default PORT if unspecified
|
|
8
|
+
dotenv.config({ path: resolve(process.cwd(), '.env') });
|
|
9
|
+
|
|
10
|
+
const createExposeCommand = (): Command => {
|
|
11
|
+
const command = new Command('expose')
|
|
12
|
+
.alias('exp')
|
|
13
|
+
.description('Expose your local ZinTrust server to the internet using secure tunnels.')
|
|
14
|
+
.argument('[port]', 'The local port to expose', process.env['PORT'] || '3000')
|
|
15
|
+
.option('--https', 'Connect locally via HTTPS instead of HTTP', false)
|
|
16
|
+
.option('--provider <provider>', 'Tunnel provider (zintrust | cloudflare)', 'cloudflare')
|
|
17
|
+
.action(async (portArg, options) => {
|
|
18
|
+
const port = Number.parseInt(portArg, 10);
|
|
19
|
+
if (Number.isNaN(port)) {
|
|
20
|
+
console.error(`Invalid port provided: ${portArg}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(`Starting URL exposure on port ${port} using ${options.provider} provider...`);
|
|
25
|
+
|
|
26
|
+
const provider = createTunnelProvider(options.provider);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const publicUrl = await provider.start({ port, https: options.https });
|
|
30
|
+
|
|
31
|
+
console.log(`\n=============================================================`);
|
|
32
|
+
console.log(`🚀 Tunnel Established successfully!`);
|
|
33
|
+
console.log(`🌍 Public URL : \x1b[32m${publicUrl}\x1b[0m`);
|
|
34
|
+
console.log(`🔌 Local Target : ${options.https ? 'https' : 'http'}://localhost:${port}`);
|
|
35
|
+
console.log(`🛡️ Provider : ${options.provider}`);
|
|
36
|
+
console.log(`=============================================================\n`);
|
|
37
|
+
console.log(`Press Ctrl+C to disconnect from the tunnel.\n`);
|
|
38
|
+
|
|
39
|
+
process.on('SIGINT', async () => {
|
|
40
|
+
console.log(`\nDisconnecting ${options.provider} tunnel...`);
|
|
41
|
+
await provider.stop();
|
|
42
|
+
process.exit(0);
|
|
43
|
+
});
|
|
44
|
+
} catch (error: unknown) {
|
|
45
|
+
console.error(`Tunnel failed to start:`, (error as Error).message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return command;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const ExposeCommand = Object.freeze({
|
|
54
|
+
getCommand: createExposeCommand,
|
|
55
|
+
name: 'expose',
|
|
56
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CloudflareProvider } from './providers/CloudflareProvider.js';
|
|
2
|
+
import type { ITunnelProvider } from './providers/ITunnelProvider.js';
|
|
3
|
+
import { ZintrustProvider } from './providers/ZintrustProvider.js';
|
|
4
|
+
|
|
5
|
+
export function createTunnelProvider(providerName: string): ITunnelProvider {
|
|
6
|
+
switch (providerName.toLowerCase()) {
|
|
7
|
+
case 'cloudflare':
|
|
8
|
+
case 'cf':
|
|
9
|
+
return CloudflareProvider.create();
|
|
10
|
+
case 'zintrust':
|
|
11
|
+
case 'zin':
|
|
12
|
+
default:
|
|
13
|
+
return new ZintrustProvider();
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
|
+
import type { ITunnelProvider, TunnelOptions } from './ITunnelProvider.js';
|
|
3
|
+
|
|
4
|
+
type ResolveOnce = (url: string) => void;
|
|
5
|
+
type RejectOnce = (error: Error) => void;
|
|
6
|
+
|
|
7
|
+
const createTunnelUrlHandler = (urlRegex: RegExp, resolveOnce: ResolveOnce) => {
|
|
8
|
+
return (data: string | Uint8Array): void => {
|
|
9
|
+
const match = urlRegex.exec(data.toString());
|
|
10
|
+
if (match !== null) {
|
|
11
|
+
resolveOnce(match[1]);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const createTunnelCloseHandler = (
|
|
17
|
+
clearStartupTimer: () => void,
|
|
18
|
+
shouldReject: () => boolean,
|
|
19
|
+
rejectOnce: RejectOnce
|
|
20
|
+
) => {
|
|
21
|
+
return (code: number | null): void => {
|
|
22
|
+
clearStartupTimer();
|
|
23
|
+
if (shouldReject() && code !== 0) {
|
|
24
|
+
rejectOnce(new Error(`Tunnel closed unexpectedly with code ${String(code)}`));
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const createTunnelErrorHandler = (rejectOnce: RejectOnce) => {
|
|
30
|
+
return (error: Error): void => {
|
|
31
|
+
rejectOnce(error);
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const rejectStartupTimeout = (rejectOnce: RejectOnce): void => {
|
|
36
|
+
rejectOnce(new Error('Cloudflared tunnel startup timed out.'));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const stopAfterStartupTimeout = async (
|
|
40
|
+
stop: () => Promise<void>,
|
|
41
|
+
rejectOnce: RejectOnce
|
|
42
|
+
): Promise<void> => {
|
|
43
|
+
try {
|
|
44
|
+
await stop();
|
|
45
|
+
} finally {
|
|
46
|
+
rejectStartupTimeout(rejectOnce);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const createStartupTimeoutHandler = (stop: () => Promise<void>, rejectOnce: RejectOnce) => {
|
|
51
|
+
return (): void => {
|
|
52
|
+
void stopAfterStartupTimeout(stop, rejectOnce);
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const create = (): ITunnelProvider => {
|
|
57
|
+
let childProcess: ChildProcess | null = null;
|
|
58
|
+
let isStopping = false;
|
|
59
|
+
let startupTimer: NodeJS.Timeout | undefined;
|
|
60
|
+
|
|
61
|
+
const clearStartupTimer = (): void => {
|
|
62
|
+
if (startupTimer !== undefined) {
|
|
63
|
+
clearTimeout(startupTimer);
|
|
64
|
+
startupTimer = undefined;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const stop = async (): Promise<void> => {
|
|
69
|
+
isStopping = true;
|
|
70
|
+
clearStartupTimer();
|
|
71
|
+
|
|
72
|
+
if (childProcess !== null) {
|
|
73
|
+
childProcess.kill();
|
|
74
|
+
childProcess = null;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const start = async (options: TunnelOptions): Promise<string> => {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const scheme = options.https ? 'https' : 'http';
|
|
81
|
+
const localUrl = `${scheme}://localhost:${options.port}`;
|
|
82
|
+
const urlRegex = /(https:\/\/[a-z0-9-]+\.trycloudflare\.com)/;
|
|
83
|
+
let settled = false;
|
|
84
|
+
|
|
85
|
+
const resolveOnce = (url: string): void => {
|
|
86
|
+
if (settled) return;
|
|
87
|
+
settled = true;
|
|
88
|
+
clearStartupTimer();
|
|
89
|
+
resolve(url);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const rejectOnce = (error: Error): void => {
|
|
93
|
+
if (settled) return;
|
|
94
|
+
settled = true;
|
|
95
|
+
clearStartupTimer();
|
|
96
|
+
reject(error);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
process.stdout.write(`Starting cloudflared tunnel to ${localUrl}...\n`);
|
|
100
|
+
|
|
101
|
+
childProcess = spawn('npx', ['cloudflared', 'tunnel', '--url', localUrl], {
|
|
102
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const handleTunnelUrl = createTunnelUrlHandler(urlRegex, resolveOnce);
|
|
106
|
+
const handleClose = createTunnelCloseHandler(
|
|
107
|
+
clearStartupTimer,
|
|
108
|
+
() => !isStopping && !settled,
|
|
109
|
+
rejectOnce
|
|
110
|
+
);
|
|
111
|
+
const handleError = createTunnelErrorHandler(rejectOnce);
|
|
112
|
+
const handleStartupTimeout = createStartupTimeoutHandler(stop, rejectOnce);
|
|
113
|
+
|
|
114
|
+
childProcess.stderr?.on('data', handleTunnelUrl);
|
|
115
|
+
|
|
116
|
+
childProcess.on('close', handleClose);
|
|
117
|
+
|
|
118
|
+
childProcess.on('error', handleError);
|
|
119
|
+
|
|
120
|
+
startupTimer = globalThis.setTimeout(handleStartupTimeout, 15000);
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return Object.freeze({
|
|
125
|
+
start,
|
|
126
|
+
stop,
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const CloudflareProvider = Object.freeze({
|
|
131
|
+
create,
|
|
132
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface TunnelOptions {
|
|
2
|
+
port: number;
|
|
3
|
+
https: boolean;
|
|
4
|
+
subdomain?: string;
|
|
5
|
+
providerOptions?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ITunnelProvider {
|
|
9
|
+
/**
|
|
10
|
+
* Start the tunnel and return the public URL.
|
|
11
|
+
*/
|
|
12
|
+
start(options: TunnelOptions): Promise<string>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Stop the tunnel.
|
|
16
|
+
*/
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-syntax */
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
import type { ITunnelProvider, TunnelOptions } from './ITunnelProvider.js';
|
|
4
|
+
|
|
5
|
+
export class ZintrustProvider implements ITunnelProvider {
|
|
6
|
+
async start(_options: TunnelOptions): Promise<string> {
|
|
7
|
+
// In the future, this will connect to the official ZinTrust tunneled infrastructure
|
|
8
|
+
// via WebSockets or a similar reverse proxy technique.
|
|
9
|
+
// For now, we stub this out or fallback to locolhost.
|
|
10
|
+
console.warn('ZinTrust provider is coming soon in the expose package.');
|
|
11
|
+
console.warn('Please use `--provider cloudflare` instead for now.');
|
|
12
|
+
throw new Error('ZinTrust Tunnel Provider not implemented yet');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async stop(): Promise<void> {
|
|
16
|
+
// Clean up WS connections or sockets
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ExposeCommand } from './ExposeCommand.js';
|
|
2
|
+
|
|
3
|
+
type OptionalCliCommandRegistryLike = {
|
|
4
|
+
register: (name: string, command: typeof ExposeCommand) => void;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type CoreModuleLike = {
|
|
8
|
+
OptionalCliCommandRegistry?: OptionalCliCommandRegistryLike;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const core = (await import('@zintrust/core')) as CoreModuleLike;
|
|
13
|
+
|
|
14
|
+
if (core.OptionalCliCommandRegistry !== undefined) {
|
|
15
|
+
core.OptionalCliCommandRegistry.register('expose', ExposeCommand);
|
|
16
|
+
}
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
19
|
+
process.stderr.write(`Failed to register expose command: ${message}\n`);
|
|
20
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"lib": ["es2023"],
|
|
6
|
+
"baseUrl": ".",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"declarationMap": false,
|
|
12
|
+
"sourceMap": false,
|
|
13
|
+
"composite": false,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"types": ["node"],
|
|
16
|
+
"tsBuildInfoFile": "./dist/.tsbuildinfo",
|
|
17
|
+
"paths": {}
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*.ts"],
|
|
20
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
21
|
+
}
|