@instafy/cli 0.1.0-staging.138

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/dist/tunnel.js ADDED
@@ -0,0 +1,180 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import kleur from "kleur";
6
+ import { resolveRatholeBinaryForCli } from "./runtime.js";
7
+ function cleanUrl(raw) {
8
+ return raw.replace(/\/+$/, "");
9
+ }
10
+ function readEnv(key) {
11
+ const value = process.env[key];
12
+ if (value && value.trim())
13
+ return value.trim();
14
+ return undefined;
15
+ }
16
+ function resolveProject(opts) {
17
+ return (opts.project?.trim() ||
18
+ readEnv("PROJECT_ID") ||
19
+ readEnv("CONTROLLER_PROJECT_ID") ||
20
+ (() => {
21
+ throw new Error("Project id is required (--project or PROJECT_ID)");
22
+ })());
23
+ }
24
+ function resolveControllerUrl(opts) {
25
+ return (opts.controllerUrl?.trim() ||
26
+ readEnv("CONTROLLER_URL") ||
27
+ readEnv("CONTROLLER_BASE_URL") ||
28
+ "http://localhost:8788");
29
+ }
30
+ function resolveControllerToken(opts) {
31
+ return (opts.controllerToken?.trim() ||
32
+ readEnv("CONTROLLER_BEARER") ||
33
+ readEnv("SERVICE_ROLE_KEY") ||
34
+ readEnv("SUPABASE_SERVICE_ROLE_KEY") ||
35
+ readEnv("CONTROLLER_INTERNAL_TOKEN") ||
36
+ (() => {
37
+ throw new Error("Controller token is required (--controller-token or SERVICE_ROLE_KEY)");
38
+ })());
39
+ }
40
+ function resolvePort(opts) {
41
+ const fromEnv = readEnv("WEBHOOK_LOCAL_PORT") ||
42
+ readEnv("TUNNEL_LOCAL_PORT") ||
43
+ undefined;
44
+ const parsedEnv = fromEnv ? Number(fromEnv) : NaN;
45
+ return opts.port ?? (Number.isFinite(parsedEnv) ? parsedEnv : 3000);
46
+ }
47
+ async function requestTunnel(controllerUrl, token, projectId, metadata) {
48
+ const target = `${cleanUrl(controllerUrl)}/projects/${encodeURIComponent(projectId)}/tunnels/request`;
49
+ const response = await fetch(target, {
50
+ method: "POST",
51
+ headers: {
52
+ authorization: `Bearer ${token}`,
53
+ "content-type": "application/json",
54
+ accept: "application/json",
55
+ },
56
+ body: JSON.stringify({ metadata }),
57
+ });
58
+ if (!response.ok) {
59
+ const text = await response.text().catch(() => "");
60
+ throw new Error(`Tunnel request failed (${response.status} ${response.statusText}): ${text}`);
61
+ }
62
+ const body = (await response.json());
63
+ const tunnelId = typeof body["tunnelId"] === "string"
64
+ ? body["tunnelId"]
65
+ : typeof body["tunnel_id"] === "string"
66
+ ? body["tunnel_id"]
67
+ : null;
68
+ const hostname = typeof body["hostname"] === "string" ? body["hostname"] : null;
69
+ if (!tunnelId || !hostname) {
70
+ throw new Error("Tunnel response missing tunnelId/hostname");
71
+ }
72
+ return {
73
+ tunnelId,
74
+ hostname,
75
+ url: typeof body["url"] === "string" ? body["url"] : undefined,
76
+ credentials: (body["credentials"] ?? null),
77
+ };
78
+ }
79
+ async function revokeTunnel(controllerUrl, token, projectId, tunnelId) {
80
+ const target = `${cleanUrl(controllerUrl)}/projects/${encodeURIComponent(projectId)}/tunnels/${encodeURIComponent(tunnelId)}/revoke`;
81
+ await fetch(target, {
82
+ method: "POST",
83
+ headers: {
84
+ authorization: `Bearer ${token}`,
85
+ "content-type": "application/json",
86
+ },
87
+ body: JSON.stringify({ metadata: { reason: "instafy-cli-tunnel:stop" } }),
88
+ }).catch(() => { });
89
+ }
90
+ function buildRatholeConfig(creds, port) {
91
+ const server = typeof creds["server"] === "string" ? creds["server"] : null;
92
+ const token = typeof creds["token"] === "string" ? creds["token"] : null;
93
+ const service = (typeof creds["service"] === "string" && creds["service"]) ||
94
+ (typeof creds["serviceName"] === "string" && creds["serviceName"]) ||
95
+ "runtime";
96
+ const protocol = (typeof creds["protocol"] === "string" && creds["protocol"]) || "tcp";
97
+ if (!server || !token) {
98
+ throw new Error("Tunnel credentials missing server/token");
99
+ }
100
+ return (`[client]
101
+ remote_addr = "${server}"
102
+ default_token = "${token}"
103
+
104
+ [client.services.${service}]
105
+ type = "${protocol}"
106
+ local_addr = "127.0.0.1:${port}"
107
+ `);
108
+ }
109
+ export async function startTunnelSession(opts) {
110
+ const projectId = resolveProject(opts);
111
+ const controllerUrl = resolveControllerUrl(opts);
112
+ const controllerToken = resolveControllerToken(opts);
113
+ const port = resolvePort(opts);
114
+ let cleanedUp = false;
115
+ if (opts.ratholeBin) {
116
+ process.env.RATHOLE_BIN = opts.ratholeBin;
117
+ }
118
+ const rathole = await resolveRatholeBinaryForCli({
119
+ env: process.env,
120
+ version: process.env.RATHOLE_VERSION ?? null,
121
+ cacheDir: process.env.RATHOLE_CACHE_DIR ?? null,
122
+ logger: (message) => console.log(kleur.cyan(`[rathole] ${message}`)),
123
+ warn: (message) => console.warn(kleur.yellow(message)),
124
+ });
125
+ if (!rathole) {
126
+ throw new Error("rathole is required to start a tunnel. Set RATHOLE_BIN or ensure it is on PATH.");
127
+ }
128
+ const metadata = { localPort: port, source: "instafy-cli" };
129
+ const grant = await requestTunnel(controllerUrl, controllerToken, projectId, metadata);
130
+ const creds = grant.credentials && typeof grant.credentials === "object"
131
+ ? grant.credentials
132
+ : {};
133
+ const configBody = buildRatholeConfig(creds, port);
134
+ const workdir = fs.mkdtempSync(path.join(os.tmpdir(), "instafy-cli-tunnel-"));
135
+ const configPath = path.join(workdir, "rathole.toml");
136
+ fs.writeFileSync(configPath, configBody, "utf8");
137
+ const child = spawn(rathole, ["-c", configPath], {
138
+ stdio: "inherit",
139
+ cwd: workdir,
140
+ });
141
+ const cleanup = async () => {
142
+ if (cleanedUp)
143
+ return;
144
+ cleanedUp = true;
145
+ child.kill("SIGTERM");
146
+ await revokeTunnel(controllerUrl, controllerToken, projectId, grant.tunnelId);
147
+ fs.rmSync(workdir, { recursive: true, force: true });
148
+ };
149
+ child.on("exit", () => {
150
+ void cleanup();
151
+ });
152
+ return {
153
+ url: grant.url ?? `https://${grant.hostname}`,
154
+ hostname: grant.hostname,
155
+ tunnelId: grant.tunnelId,
156
+ close: cleanup,
157
+ };
158
+ }
159
+ export async function runTunnelCommand(opts, options) {
160
+ const session = await startTunnelSession(opts);
161
+ console.log(kleur.green(`Tunnel ready: ${session.url} (tunnelId=${session.tunnelId})`));
162
+ const timeoutMs = options?.timeoutMs ?? null;
163
+ await new Promise((resolve) => {
164
+ let timeout = null;
165
+ if (timeoutMs && timeoutMs > 0) {
166
+ timeout = setTimeout(async () => {
167
+ await session.close();
168
+ resolve();
169
+ }, timeoutMs);
170
+ }
171
+ const handle = async () => {
172
+ await session.close();
173
+ if (timeout)
174
+ clearTimeout(timeout);
175
+ resolve();
176
+ };
177
+ process.once("SIGINT", handle);
178
+ process.once("SIGTERM", handle);
179
+ });
180
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@instafy/cli",
3
+ "version": "0.1.0-staging.138",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "bin": {
14
+ "instafy": "bin/instafy.js"
15
+ },
16
+ "type": "module",
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "dev": "ts-node src/index.ts",
20
+ "test": "pnpm test:unit",
21
+ "test:unit": "pnpm build && vitest run",
22
+ "test:live": "pnpm build && TUNNEL_E2E_LIVE=1 vitest run test/tunnel-live.e2e.spec.ts"
23
+ },
24
+ "dependencies": {
25
+ "commander": "^12.1.0",
26
+ "kleur": "^4.1.5"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.8.1",
30
+ "ts-node": "^10.9.2",
31
+ "typescript": "^5.4.5",
32
+ "vitest": "^1.6.0"
33
+ }
34
+ }