@spawn-dock/dev-tunnel 1.0.0-canary.20260320130238.da674ff
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 +60 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +117 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +48 -0
- package/dist/protocol.js +17 -0
- package/dist/protocol.js.map +1 -0
- package/dist/proxy.d.ts +2 -0
- package/dist/proxy.js +50 -0
- package/dist/proxy.js.map +1 -0
- package/dist/tunnel.d.ts +3 -0
- package/dist/tunnel.js +79 -0
- package/dist/tunnel.js.map +1 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# `@spawn-dock/dev-tunnel`
|
|
2
|
+
|
|
3
|
+
WebSocket tunnel client for SpawnDock local development preview. Exposes your local dev server through the SpawnDock control plane so others can preview your Telegram Mini App.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @spawn-dock/dev-tunnel
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use with npx:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @spawn-dock/dev-tunnel
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### With `spawndock.dev-tunnel.json` (recommended)
|
|
20
|
+
|
|
21
|
+
If your project has a `spawndock.dev-tunnel.json` file (created by the bootstrap CLI), just run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @spawn-dock/dev-tunnel
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### With CLI arguments
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx @spawn-dock/dev-tunnel \
|
|
31
|
+
--control-plane http://your-server:3000 \
|
|
32
|
+
--project-slug my-app \
|
|
33
|
+
--device-secret your-device-secret \
|
|
34
|
+
--port 3000
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### With environment variables
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
export SPAWNDOCK_CONTROL_PLANE=http://your-server:3000
|
|
41
|
+
export SPAWNDOCK_PROJECT_SLUG=my-app
|
|
42
|
+
export SPAWNDOCK_DEVICE_SECRET=your-device-secret
|
|
43
|
+
export SPAWNDOCK_PORT=3000
|
|
44
|
+
npx @spawn-dock/dev-tunnel
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuration Priority
|
|
48
|
+
|
|
49
|
+
CLI arguments > Environment variables > `spawndock.dev-tunnel.json` > legacy `spawndock.config.json`
|
|
50
|
+
|
|
51
|
+
## How it works
|
|
52
|
+
|
|
53
|
+
1. Connects to the SpawnDock control plane via WebSocket
|
|
54
|
+
2. Receives HTTP requests from users viewing your preview URL
|
|
55
|
+
3. Proxies those requests to your local dev server
|
|
56
|
+
4. Sends responses back through the tunnel
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const PRIMARY_CONFIG_FILE = "spawndock.dev-tunnel.json";
|
|
4
|
+
const LEGACY_CONFIG_FILE = "spawndock.config.json";
|
|
5
|
+
function readNumber(value) {
|
|
6
|
+
if (value === undefined || value.length === 0) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const parsed = Number.parseInt(value, 10);
|
|
10
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
11
|
+
}
|
|
12
|
+
function normalizeConfig(data) {
|
|
13
|
+
if (typeof data !== "object" || data === null) {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
const record = data;
|
|
17
|
+
const controlPlane = typeof record.controlPlane === "string"
|
|
18
|
+
? record.controlPlane
|
|
19
|
+
: typeof record.controlPlaneUrl === "string"
|
|
20
|
+
? record.controlPlaneUrl
|
|
21
|
+
: undefined;
|
|
22
|
+
const projectSlug = typeof record.projectSlug === "string" ? record.projectSlug : undefined;
|
|
23
|
+
const deviceSecret = typeof record.deviceSecret === "string"
|
|
24
|
+
? record.deviceSecret
|
|
25
|
+
: typeof record.deviceToken === "string"
|
|
26
|
+
? record.deviceToken
|
|
27
|
+
: undefined;
|
|
28
|
+
const port = typeof record.port === "number"
|
|
29
|
+
? record.port
|
|
30
|
+
: typeof record.localPort === "number"
|
|
31
|
+
? record.localPort
|
|
32
|
+
: undefined;
|
|
33
|
+
return { controlPlane, projectSlug, deviceSecret, port };
|
|
34
|
+
}
|
|
35
|
+
function readConfigFile(dir) {
|
|
36
|
+
for (const fileName of [PRIMARY_CONFIG_FILE, LEGACY_CONFIG_FILE]) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = readFileSync(resolve(dir, fileName), "utf-8");
|
|
39
|
+
return normalizeConfig(JSON.parse(raw));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Try next candidate.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
function parseArgs(argv) {
|
|
48
|
+
const result = {};
|
|
49
|
+
for (let i = 0; i < argv.length; i++) {
|
|
50
|
+
const arg = argv[i];
|
|
51
|
+
const next = argv[i + 1];
|
|
52
|
+
if (arg === "--control-plane" || arg.startsWith("--control-plane=")) {
|
|
53
|
+
const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : next;
|
|
54
|
+
if (value) {
|
|
55
|
+
result.controlPlane = value;
|
|
56
|
+
}
|
|
57
|
+
if (!arg.includes("=")) {
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (arg === "--project-slug" || arg.startsWith("--project-slug=")) {
|
|
63
|
+
const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : next;
|
|
64
|
+
if (value) {
|
|
65
|
+
result.projectSlug = value;
|
|
66
|
+
}
|
|
67
|
+
if (!arg.includes("=")) {
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg === "--device-secret" || arg.startsWith("--device-secret=")) {
|
|
73
|
+
const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : next;
|
|
74
|
+
if (value) {
|
|
75
|
+
result.deviceSecret = value;
|
|
76
|
+
}
|
|
77
|
+
if (!arg.includes("=")) {
|
|
78
|
+
i++;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (arg === "--port" || arg.startsWith("--port=")) {
|
|
83
|
+
const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : next;
|
|
84
|
+
const parsed = readNumber(value);
|
|
85
|
+
if (parsed !== undefined) {
|
|
86
|
+
result.port = parsed;
|
|
87
|
+
}
|
|
88
|
+
if (!arg.includes("=")) {
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
export function resolveConfig(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
96
|
+
const file = readConfigFile(cwd);
|
|
97
|
+
const args = parseArgs(argv);
|
|
98
|
+
const env = {
|
|
99
|
+
controlPlane: process.env.SPAWNDOCK_CONTROL_PLANE,
|
|
100
|
+
projectSlug: process.env.SPAWNDOCK_PROJECT_SLUG,
|
|
101
|
+
deviceSecret: process.env.SPAWNDOCK_DEVICE_SECRET,
|
|
102
|
+
port: readNumber(process.env.SPAWNDOCK_PORT),
|
|
103
|
+
};
|
|
104
|
+
// Priority: CLI > Env > File
|
|
105
|
+
const controlPlane = args.controlPlane ?? env.controlPlane ?? file.controlPlane;
|
|
106
|
+
const projectSlug = args.projectSlug ?? env.projectSlug ?? file.projectSlug;
|
|
107
|
+
const deviceSecret = args.deviceSecret ?? env.deviceSecret ?? file.deviceSecret;
|
|
108
|
+
const port = args.port ?? env.port ?? file.port ?? 3000;
|
|
109
|
+
if (!controlPlane)
|
|
110
|
+
throw new Error("Missing --control-plane or SPAWNDOCK_CONTROL_PLANE");
|
|
111
|
+
if (!projectSlug)
|
|
112
|
+
throw new Error("Missing --project-slug or SPAWNDOCK_PROJECT_SLUG");
|
|
113
|
+
if (!deviceSecret)
|
|
114
|
+
throw new Error("Missing --device-secret or SPAWNDOCK_DEVICE_SECRET");
|
|
115
|
+
return { controlPlane, projectSlug, deviceSecret, port };
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolveConfig } from "./config.js";
|
|
3
|
+
import { createTunnel } from "./tunnel.js";
|
|
4
|
+
try {
|
|
5
|
+
const config = resolveConfig();
|
|
6
|
+
console.log(`SpawnDock dev tunnel: ${config.projectSlug} -> http://127.0.0.1:${config.port}`);
|
|
7
|
+
createTunnel(config);
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
console.error(`Error: ${err.message}`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,IAAI,CAAC;IACH,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,OAAO,CAAC,GAAG,CACT,yBAAyB,MAAM,CAAC,WAAW,wBAAwB,MAAM,CAAC,IAAI,EAAE,CACjF,CAAC;IACF,YAAY,CAAC,MAAM,CAAC,CAAC;AACvB,CAAC;AAAC,OAAO,GAAQ,EAAE,CAAC;IAClB,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IACvC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface SerializedHttpRequest {
|
|
2
|
+
requestId: string;
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
headers: [string, string][];
|
|
6
|
+
body?: {
|
|
7
|
+
encoding: "utf8" | "base64";
|
|
8
|
+
value: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface SerializedHttpResponse {
|
|
12
|
+
requestId: string;
|
|
13
|
+
status: number;
|
|
14
|
+
headers: [string, string][];
|
|
15
|
+
body?: {
|
|
16
|
+
encoding: "utf8" | "base64";
|
|
17
|
+
value: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export type InboundMessage = {
|
|
21
|
+
type: "http-request";
|
|
22
|
+
request: SerializedHttpRequest;
|
|
23
|
+
} | {
|
|
24
|
+
type: "ping";
|
|
25
|
+
nonce: string;
|
|
26
|
+
};
|
|
27
|
+
export type OutboundMessage = {
|
|
28
|
+
type: "hello";
|
|
29
|
+
projectSlug: string;
|
|
30
|
+
port: number;
|
|
31
|
+
protocolVersion: 1;
|
|
32
|
+
} | {
|
|
33
|
+
type: "heartbeat";
|
|
34
|
+
projectSlug: string;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
} | {
|
|
37
|
+
type: "http-response";
|
|
38
|
+
response: SerializedHttpResponse;
|
|
39
|
+
} | {
|
|
40
|
+
type: "pong";
|
|
41
|
+
nonce: string;
|
|
42
|
+
} | {
|
|
43
|
+
type: "error";
|
|
44
|
+
requestId?: string;
|
|
45
|
+
message: string;
|
|
46
|
+
};
|
|
47
|
+
export declare function parseInbound(data: string): InboundMessage | null;
|
|
48
|
+
export declare function serialize(msg: OutboundMessage): string;
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function parseInbound(data) {
|
|
2
|
+
try {
|
|
3
|
+
const msg = JSON.parse(data);
|
|
4
|
+
if (msg.type === "http-request" && msg.request)
|
|
5
|
+
return msg;
|
|
6
|
+
if (msg.type === "ping" && typeof msg.nonce === "string")
|
|
7
|
+
return msg;
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function serialize(msg) {
|
|
15
|
+
return JSON.stringify(msg);
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AA2BA,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,GAAG,CAAC,IAAI,KAAK,cAAc,IAAI,GAAG,CAAC,OAAO;YAAE,OAAO,GAAG,CAAC;QAC3D,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO,GAAG,CAAC;QACrE,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAoB;IAC5C,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC"}
|
package/dist/proxy.d.ts
ADDED
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const HOP_BY_HOP = new Set([
|
|
2
|
+
"connection", "host", "keep-alive", "proxy-authenticate",
|
|
3
|
+
"proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", "content-length",
|
|
4
|
+
]);
|
|
5
|
+
function decodeBody(body) {
|
|
6
|
+
if (body.encoding === "utf8")
|
|
7
|
+
return body.value;
|
|
8
|
+
const buf = Buffer.from(body.value, "base64");
|
|
9
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
10
|
+
}
|
|
11
|
+
export async function proxyRequest(request, localOrigin) {
|
|
12
|
+
const url = new URL(request.path, localOrigin);
|
|
13
|
+
const headers = new Headers();
|
|
14
|
+
for (const [name, value] of request.headers) {
|
|
15
|
+
if (!HOP_BY_HOP.has(name.toLowerCase())) {
|
|
16
|
+
headers.set(name, value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const init = { method: request.method, headers };
|
|
20
|
+
const method = request.method.toUpperCase();
|
|
21
|
+
if (request.body && method !== "GET" && method !== "HEAD") {
|
|
22
|
+
init.body = decodeBody(request.body);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(url, init);
|
|
26
|
+
const bodyBytes = new Uint8Array(await res.arrayBuffer());
|
|
27
|
+
const responseHeaders = Array.from(res.headers.entries());
|
|
28
|
+
const response = {
|
|
29
|
+
requestId: request.requestId,
|
|
30
|
+
status: res.status,
|
|
31
|
+
headers: responseHeaders,
|
|
32
|
+
};
|
|
33
|
+
if (bodyBytes.byteLength > 0) {
|
|
34
|
+
return {
|
|
35
|
+
...response,
|
|
36
|
+
body: { encoding: "base64", value: Buffer.from(bodyBytes).toString("base64") },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return response;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return {
|
|
43
|
+
requestId: request.requestId,
|
|
44
|
+
status: 502,
|
|
45
|
+
headers: [["content-type", "application/json"]],
|
|
46
|
+
body: { encoding: "utf8", value: JSON.stringify({ error: "proxy_failed", message: err.message }) },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC;IACzB,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,oBAAoB;IACxD,qBAAqB,EAAE,IAAI,EAAE,SAAS,EAAE,mBAAmB,EAAE,SAAS,EAAE,gBAAgB;CACzF,CAAC,CAAC;AAEH,SAAS,UAAU,CAAC,IAAyC;IAC3D,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC;IAChD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC9C,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAA8B,EAC9B,WAAmB;IAEnB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAC9B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QAC5C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACxC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAgB,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;IAC9D,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;IAC5C,IAAI,OAAO,CAAC,IAAI,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QAC1D,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC;IAED,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,SAAS,GAAG,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;QAC1D,MAAM,eAAe,GAAuB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QAE9E,MAAM,QAAQ,GAA2B;YACvC,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,OAAO,EAAE,eAAe;SACzB,CAAC;QAEF,IAAI,SAAS,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;YAC7B,OAAO;gBACL,GAAG,QAAQ;gBACX,IAAI,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;aAC/E,CAAC;QACJ,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO;YACL,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,CAAC,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;YAC/C,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,EAAE;SACnG,CAAC;IACJ,CAAC;AACH,CAAC"}
|
package/dist/tunnel.d.ts
ADDED
package/dist/tunnel.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { parseInbound, serialize } from "./protocol.js";
|
|
3
|
+
import { proxyRequest } from "./proxy.js";
|
|
4
|
+
export function createTunnel(config) {
|
|
5
|
+
const localOrigin = `http://127.0.0.1:${config.port}`;
|
|
6
|
+
const wsUrl = buildWsUrl(config);
|
|
7
|
+
connect(wsUrl, config, localOrigin);
|
|
8
|
+
}
|
|
9
|
+
export function buildWsUrl(config) {
|
|
10
|
+
if (!URL.canParse(config.controlPlane)) {
|
|
11
|
+
throw new Error(`Invalid control plane URL: ${config.controlPlane}`);
|
|
12
|
+
}
|
|
13
|
+
const url = new URL(config.controlPlane);
|
|
14
|
+
if (url.protocol === "http:") {
|
|
15
|
+
url.protocol = "ws:";
|
|
16
|
+
}
|
|
17
|
+
else if (url.protocol === "https:") {
|
|
18
|
+
url.protocol = "wss:";
|
|
19
|
+
}
|
|
20
|
+
const currentPath = url.pathname.replace(/\/+$/, "");
|
|
21
|
+
url.pathname = currentPath.length === 0 ? "/tunnel/connect" : `${currentPath}/tunnel/connect`;
|
|
22
|
+
url.searchParams.set("protocolVersion", "1");
|
|
23
|
+
url.searchParams.set("token", config.deviceSecret);
|
|
24
|
+
return url.toString();
|
|
25
|
+
}
|
|
26
|
+
function connect(wsUrl, config, localOrigin) {
|
|
27
|
+
const ws = new WebSocket(wsUrl);
|
|
28
|
+
let heartbeatInterval = null;
|
|
29
|
+
ws.on("open", () => {
|
|
30
|
+
console.log(`Connected to ${config.controlPlane}`);
|
|
31
|
+
ws.send(serialize({
|
|
32
|
+
type: "hello",
|
|
33
|
+
projectSlug: config.projectSlug,
|
|
34
|
+
port: config.port,
|
|
35
|
+
protocolVersion: 1,
|
|
36
|
+
}));
|
|
37
|
+
heartbeatInterval = setInterval(() => {
|
|
38
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
39
|
+
ws.send(serialize({
|
|
40
|
+
type: "heartbeat",
|
|
41
|
+
projectSlug: config.projectSlug,
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
}, 15_000);
|
|
46
|
+
});
|
|
47
|
+
ws.on("message", async (data) => {
|
|
48
|
+
const msg = parseInbound(data.toString());
|
|
49
|
+
if (!msg)
|
|
50
|
+
return;
|
|
51
|
+
if (msg.type === "ping") {
|
|
52
|
+
ws.send(serialize({ type: "pong", nonce: msg.nonce }));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (msg.type === "http-request") {
|
|
56
|
+
try {
|
|
57
|
+
const response = await proxyRequest(msg.request, localOrigin);
|
|
58
|
+
ws.send(serialize({ type: "http-response", response }));
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
ws.send(serialize({
|
|
62
|
+
type: "error",
|
|
63
|
+
requestId: msg.request.requestId,
|
|
64
|
+
message: err.message,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
ws.on("close", () => {
|
|
70
|
+
if (heartbeatInterval)
|
|
71
|
+
clearInterval(heartbeatInterval);
|
|
72
|
+
console.log("Disconnected. Reconnecting in 2s...");
|
|
73
|
+
setTimeout(() => connect(wsUrl, config, localOrigin), 2000);
|
|
74
|
+
});
|
|
75
|
+
ws.on("error", (err) => {
|
|
76
|
+
console.error(`WebSocket error: ${err.message}`);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=tunnel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../src/tunnel.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spawn-dock/dev-tunnel",
|
|
3
|
+
"version": "1.0.0-canary.20260320130238.da674ff",
|
|
4
|
+
"description": "WebSocket tunnel client for SpawnDock local development preview",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"spawn-dock-tunnel": "./dist/index.js",
|
|
8
|
+
"spawndock-tunnel": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"test": "vitest run"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"ws": "^8.18.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22.0.0",
|
|
26
|
+
"@types/ws": "^8.5.0",
|
|
27
|
+
"typescript": "^5.9.0",
|
|
28
|
+
"vitest": "^4.1.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"spawndock",
|
|
32
|
+
"tunnel",
|
|
33
|
+
"telegram-mini-app",
|
|
34
|
+
"tma",
|
|
35
|
+
"dev-tunnel"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|