@tiflis-io/tiflis-code-tunnel 0.3.5 → 0.3.7
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/main.js +140 -15
- package/package.json +7 -6
package/dist/main.js
CHANGED
|
@@ -84,7 +84,13 @@ var EnvSchema = z.object({
|
|
|
84
84
|
/**
|
|
85
85
|
* Custom WebSocket path (defaults to /ws).
|
|
86
86
|
*/
|
|
87
|
-
WS_PATH: z.string().default("/ws")
|
|
87
|
+
WS_PATH: z.string().default("/ws"),
|
|
88
|
+
/**
|
|
89
|
+
* Optional path to web client static files directory.
|
|
90
|
+
* When set, the tunnel server will serve the web client at the root path.
|
|
91
|
+
* Example: "./node_modules/@tiflis-io/tiflis-code-web/dist"
|
|
92
|
+
*/
|
|
93
|
+
WEB_CLIENT_PATH: z.string().optional()
|
|
88
94
|
});
|
|
89
95
|
function loadEnv() {
|
|
90
96
|
const result = EnvSchema.safeParse(process.env);
|
|
@@ -120,16 +126,16 @@ function getProtocolVersion() {
|
|
|
120
126
|
return `${PROTOCOL_VERSION.major}.${PROTOCOL_VERSION.minor}.${PROTOCOL_VERSION.patch}`;
|
|
121
127
|
}
|
|
122
128
|
var CONNECTION_TIMING = {
|
|
123
|
-
/** How often clients should send ping (
|
|
124
|
-
PING_INTERVAL_MS:
|
|
125
|
-
/** Max time to wait for ping before considering connection stale (
|
|
126
|
-
PONG_TIMEOUT_MS:
|
|
127
|
-
/** Interval for checking timed-out connections (
|
|
128
|
-
TIMEOUT_CHECK_INTERVAL_MS:
|
|
129
|
-
/** Minimum reconnect delay (
|
|
130
|
-
RECONNECT_DELAY_MIN_MS:
|
|
131
|
-
/** Maximum reconnect delay (
|
|
132
|
-
RECONNECT_DELAY_MAX_MS:
|
|
129
|
+
/** How often clients should send ping (5 seconds - fast liveness detection) */
|
|
130
|
+
PING_INTERVAL_MS: 5e3,
|
|
131
|
+
/** Max time to wait for ping before considering connection stale (15 seconds = 3 missed pings) */
|
|
132
|
+
PONG_TIMEOUT_MS: 15e3,
|
|
133
|
+
/** Interval for checking timed-out connections (5 seconds - faster cleanup) */
|
|
134
|
+
TIMEOUT_CHECK_INTERVAL_MS: 5e3,
|
|
135
|
+
/** Minimum reconnect delay (500ms - fast first retry) */
|
|
136
|
+
RECONNECT_DELAY_MIN_MS: 500,
|
|
137
|
+
/** Maximum reconnect delay (5 seconds - don't wait too long) */
|
|
138
|
+
RECONNECT_DELAY_MAX_MS: 5e3
|
|
133
139
|
};
|
|
134
140
|
var WEBSOCKET_CONFIG = {
|
|
135
141
|
/** Path for WebSocket endpoint */
|
|
@@ -519,6 +525,114 @@ async function handleError(error, reply, log) {
|
|
|
519
525
|
});
|
|
520
526
|
}
|
|
521
527
|
|
|
528
|
+
// src/infrastructure/http/web-client-route.ts
|
|
529
|
+
import fastifyStatic from "@fastify/static";
|
|
530
|
+
import { existsSync, statSync } from "fs";
|
|
531
|
+
import { resolve, join as join2 } from "path";
|
|
532
|
+
var SECURITY_HEADERS = {
|
|
533
|
+
// Prevent MIME type sniffing
|
|
534
|
+
"X-Content-Type-Options": "nosniff",
|
|
535
|
+
// Prevent clickjacking
|
|
536
|
+
"X-Frame-Options": "DENY",
|
|
537
|
+
// XSS protection (legacy, but still useful for older browsers)
|
|
538
|
+
"X-XSS-Protection": "1; mode=block",
|
|
539
|
+
// Referrer policy
|
|
540
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
541
|
+
// Permissions policy (restrict sensitive APIs)
|
|
542
|
+
"Permissions-Policy": "camera=(), microphone=(self), geolocation=(), payment=()",
|
|
543
|
+
// Content Security Policy
|
|
544
|
+
"Content-Security-Policy": [
|
|
545
|
+
"default-src 'self'",
|
|
546
|
+
"script-src 'self'",
|
|
547
|
+
"style-src 'self' 'unsafe-inline'",
|
|
548
|
+
// unsafe-inline needed for Tailwind
|
|
549
|
+
"img-src 'self' data: blob:",
|
|
550
|
+
"font-src 'self'",
|
|
551
|
+
"connect-src 'self' ws: wss:",
|
|
552
|
+
"media-src 'self' blob:",
|
|
553
|
+
"worker-src 'self' blob:",
|
|
554
|
+
"frame-ancestors 'none'",
|
|
555
|
+
"base-uri 'self'",
|
|
556
|
+
"form-action 'self'"
|
|
557
|
+
].join("; ")
|
|
558
|
+
};
|
|
559
|
+
var CACHE_CONTROL = {
|
|
560
|
+
// HTML files: no cache (SPA routing)
|
|
561
|
+
html: "no-cache, no-store, must-revalidate",
|
|
562
|
+
// Hashed assets: long cache (1 year)
|
|
563
|
+
assets: "public, max-age=31536000, immutable",
|
|
564
|
+
// Other static files: short cache (1 hour)
|
|
565
|
+
default: "public, max-age=3600"
|
|
566
|
+
};
|
|
567
|
+
function getCacheControl(url) {
|
|
568
|
+
if (url.endsWith(".html") || url === "/") {
|
|
569
|
+
return CACHE_CONTROL.html;
|
|
570
|
+
}
|
|
571
|
+
if (url.includes("/assets/") || url.includes(".")) {
|
|
572
|
+
const hasHash = /\.[a-f0-9]{8,}\./i.test(url);
|
|
573
|
+
return hasHash ? CACHE_CONTROL.assets : CACHE_CONTROL.default;
|
|
574
|
+
}
|
|
575
|
+
return CACHE_CONTROL.default;
|
|
576
|
+
}
|
|
577
|
+
async function registerWebClientRoute(app, options) {
|
|
578
|
+
const { webClientPath, logger } = options;
|
|
579
|
+
const absolutePath = resolve(webClientPath);
|
|
580
|
+
if (!existsSync(absolutePath)) {
|
|
581
|
+
logger.error(
|
|
582
|
+
{ path: absolutePath },
|
|
583
|
+
"Web client path does not exist, skipping web client registration"
|
|
584
|
+
);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const stats = statSync(absolutePath);
|
|
588
|
+
if (!stats.isDirectory()) {
|
|
589
|
+
logger.error(
|
|
590
|
+
{ path: absolutePath },
|
|
591
|
+
"Web client path is not a directory, skipping web client registration"
|
|
592
|
+
);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const indexPath = join2(absolutePath, "index.html");
|
|
596
|
+
if (!existsSync(indexPath)) {
|
|
597
|
+
logger.error(
|
|
598
|
+
{ path: indexPath },
|
|
599
|
+
"Web client index.html not found, skipping web client registration"
|
|
600
|
+
);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
logger.info({ path: absolutePath }, "Registering web client static files");
|
|
604
|
+
app.addHook("onSend", async (request, reply, payload) => {
|
|
605
|
+
const url = request.url;
|
|
606
|
+
if (url.startsWith("/api/") || url.startsWith("/ws") || url.startsWith("/health")) {
|
|
607
|
+
return payload;
|
|
608
|
+
}
|
|
609
|
+
for (const [header, value] of Object.entries(SECURITY_HEADERS)) {
|
|
610
|
+
reply.header(header, value);
|
|
611
|
+
}
|
|
612
|
+
reply.header("Cache-Control", getCacheControl(url));
|
|
613
|
+
return payload;
|
|
614
|
+
});
|
|
615
|
+
await app.register(fastifyStatic, {
|
|
616
|
+
root: absolutePath,
|
|
617
|
+
prefix: "/",
|
|
618
|
+
// Serve index.html for SPA routing
|
|
619
|
+
wildcard: false
|
|
620
|
+
});
|
|
621
|
+
app.setNotFoundHandler(
|
|
622
|
+
async (request, reply) => {
|
|
623
|
+
const url = request.url;
|
|
624
|
+
if (url.startsWith("/api/") || url.startsWith("/ws") || url.startsWith("/health")) {
|
|
625
|
+
return reply.status(404).send({
|
|
626
|
+
error: "Not Found",
|
|
627
|
+
code: "NOT_FOUND"
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
return reply.sendFile("index.html");
|
|
631
|
+
}
|
|
632
|
+
);
|
|
633
|
+
logger.info("Web client registered successfully");
|
|
634
|
+
}
|
|
635
|
+
|
|
522
636
|
// src/domain/value-objects/tunnel-id.ts
|
|
523
637
|
var TunnelId = class _TunnelId {
|
|
524
638
|
_value;
|
|
@@ -1010,18 +1124,18 @@ var WebSocketServerWrapper = class {
|
|
|
1010
1124
|
} catch {
|
|
1011
1125
|
}
|
|
1012
1126
|
});
|
|
1013
|
-
const closePromise = new Promise((
|
|
1127
|
+
const closePromise = new Promise((resolve2, reject) => {
|
|
1014
1128
|
wss.close((err) => {
|
|
1015
1129
|
if (err) {
|
|
1016
1130
|
this.logger.error({ error: err }, "Error closing WebSocket server");
|
|
1017
1131
|
reject(err);
|
|
1018
1132
|
} else {
|
|
1019
1133
|
this.logger.info("WebSocket server closed gracefully");
|
|
1020
|
-
|
|
1134
|
+
resolve2();
|
|
1021
1135
|
}
|
|
1022
1136
|
});
|
|
1023
1137
|
});
|
|
1024
|
-
const timeoutPromise = new Promise((
|
|
1138
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
1025
1139
|
setTimeout(() => {
|
|
1026
1140
|
this.logger.warn(
|
|
1027
1141
|
{ timeoutMs, remainingClients: wss.clients.size },
|
|
@@ -1033,7 +1147,7 @@ var WebSocketServerWrapper = class {
|
|
|
1033
1147
|
} catch {
|
|
1034
1148
|
}
|
|
1035
1149
|
});
|
|
1036
|
-
|
|
1150
|
+
resolve2();
|
|
1037
1151
|
}, timeoutMs);
|
|
1038
1152
|
});
|
|
1039
1153
|
await Promise.race([closePromise, timeoutPromise]);
|
|
@@ -2287,6 +2401,12 @@ async function bootstrap() {
|
|
|
2287
2401
|
httpClientOperations,
|
|
2288
2402
|
logger
|
|
2289
2403
|
});
|
|
2404
|
+
if (env.WEB_CLIENT_PATH) {
|
|
2405
|
+
await registerWebClientRoute(app, {
|
|
2406
|
+
webClientPath: env.WEB_CLIENT_PATH,
|
|
2407
|
+
logger
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2290
2410
|
const wsServer = new WebSocketServerWrapper(
|
|
2291
2411
|
{
|
|
2292
2412
|
path: env.WS_PATH || WEBSOCKET_CONFIG.PATH,
|
|
@@ -2395,6 +2515,11 @@ bootstrap().catch((error) => {
|
|
|
2395
2515
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
2396
2516
|
* @license FSL-1.1-NC
|
|
2397
2517
|
*/
|
|
2518
|
+
/**
|
|
2519
|
+
* @file web-client-route.ts
|
|
2520
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
2521
|
+
* @license FSL-1.1-NC
|
|
2522
|
+
*/
|
|
2398
2523
|
/**
|
|
2399
2524
|
* @file tunnel-id.ts
|
|
2400
2525
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiflis-io/tiflis-code-tunnel",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Tunnel server for tiflis-code - reverse proxy for workstation connections",
|
|
5
5
|
"author": "Roman Barinov <rbarinov@gmail.com>",
|
|
6
6
|
"license": "FSL-1.1-NC",
|
|
@@ -33,21 +33,22 @@
|
|
|
33
33
|
"dist"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@fastify/static": "^8.3.0",
|
|
37
|
+
"dotenv": "^16.4.7",
|
|
36
38
|
"fastify": "^5.2.1",
|
|
37
|
-
"
|
|
38
|
-
"zod": "^3.24.1",
|
|
39
|
+
"nanoid": "^5.0.9",
|
|
39
40
|
"pino": "^9.5.0",
|
|
40
41
|
"pino-pretty": "^13.0.0",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
42
|
+
"ws": "^8.18.0",
|
|
43
|
+
"zod": "^3.24.1"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@eslint/js": "^9.17.0",
|
|
46
47
|
"@types/node": "^22.10.2",
|
|
47
48
|
"@types/ws": "^8.5.13",
|
|
48
49
|
"eslint": "^9.17.0",
|
|
49
|
-
"tsx": "^4.19.2",
|
|
50
50
|
"tsup": "^8.3.5",
|
|
51
|
+
"tsx": "^4.19.2",
|
|
51
52
|
"typescript": "^5.7.2",
|
|
52
53
|
"typescript-eslint": "^8.18.1",
|
|
53
54
|
"vitest": "^2.1.8"
|