@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 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
@@ -0,0 +1,7 @@
1
+ export interface TunnelConfig {
2
+ controlPlane: string;
3
+ projectSlug: string;
4
+ deviceSecret: string;
5
+ port: number;
6
+ }
7
+ export declare function resolveConfig(argv?: string[], cwd?: string): TunnelConfig;
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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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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;
@@ -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"}
@@ -0,0 +1,2 @@
1
+ import type { SerializedHttpRequest, SerializedHttpResponse } from "./protocol.js";
2
+ export declare function proxyRequest(request: SerializedHttpRequest, localOrigin: string): Promise<SerializedHttpResponse>;
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"}
@@ -0,0 +1,3 @@
1
+ import type { TunnelConfig } from "./config.js";
2
+ export declare function createTunnel(config: TunnelConfig): void;
3
+ export declare function buildWsUrl(config: TunnelConfig): string;
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
+ }