@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 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,5 @@
1
+ import { Command } from 'commander';
2
+ export declare const ExposeCommand: Readonly<{
3
+ getCommand: () => Command;
4
+ name: "expose";
5
+ }>;
@@ -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,2 @@
1
+ import type { ITunnelProvider } from './providers/ITunnelProvider.js';
2
+ export declare function createTunnelProvider(providerName: string): ITunnelProvider;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { ExposeCommand } from './ExposeCommand.js';
2
+ export { ITunnelProvider } from './providers/ITunnelProvider.js';
@@ -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,12 @@
1
+ /**
2
+ * @zintrust/expose v0.4.58
3
+ *
4
+ * ZinTrust local tunnel exposure package
5
+ *
6
+ * Build Information:
7
+ * Built: 2026-04-04T19:58:21.168Z
8
+ * Node: >=20.0.0
9
+ * License: MIT
10
+ *
11
+ */
12
+ export { ExposeCommand } from './ExposeCommand.js';
@@ -0,0 +1,4 @@
1
+ import type { ITunnelProvider } from './ITunnelProvider.js';
2
+ export declare const CloudflareProvider: Readonly<{
3
+ create: () => ITunnelProvider;
4
+ }>;
@@ -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,5 @@
1
+ import type { ITunnelProvider, TunnelOptions } from './ITunnelProvider.js';
2
+ export declare class ZintrustProvider implements ITunnelProvider {
3
+ start(_options: TunnelOptions): Promise<string>;
4
+ stop(): Promise<void>;
5
+ }
@@ -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":""}
@@ -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,3 @@
1
+ export { ExposeCommand } from './ExposeCommand.js';
2
+
3
+ export { ITunnelProvider } from './providers/ITunnelProvider.js';
@@ -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
+ }
@@ -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
+ }