@linkshell/gateway 0.5.2 → 0.6.0
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/Dockerfile +8 -22
- package/README.md +99 -44
- package/dist/gateway/src/accounts.js.map +1 -0
- package/dist/gateway/src/index.d.ts +4 -1
- package/dist/gateway/src/index.js +4 -863
- package/dist/gateway/src/index.js.map +1 -1
- package/dist/gateway/src/main.js +57 -0
- package/dist/gateway/src/main.js.map +1 -0
- package/dist/gateway/src/rate-limit.d.ts +22 -0
- package/dist/gateway/src/rate-limit.js +66 -0
- package/dist/gateway/src/rate-limit.js.map +1 -0
- package/dist/gateway/src/relay.d.ts +84 -5
- package/dist/gateway/src/relay.js +402 -529
- package/dist/gateway/src/relay.js.map +1 -1
- package/dist/gateway/src/serve.d.ts +34 -0
- package/dist/gateway/src/serve.js +102 -0
- package/dist/gateway/src/serve.js.map +1 -0
- package/dist/{gateway-v2 → gateway}/src/store.d.ts +5 -0
- package/dist/{gateway-v2 → gateway}/src/store.js +7 -0
- package/dist/gateway/src/store.js.map +1 -0
- package/dist/gateway/src/subscription.d.ts +18 -0
- package/dist/gateway/src/subscription.js +46 -0
- package/dist/gateway/src/subscription.js.map +1 -0
- package/dist/gateway/tsconfig.tsbuildinfo +1 -1
- package/dist/wire/src/direct.d.ts +22 -0
- package/dist/wire/src/direct.js +37 -0
- package/dist/wire/src/direct.js.map +1 -0
- package/dist/wire/src/index.d.ts +1 -0
- package/dist/wire/src/index.js +1 -0
- package/dist/wire/src/index.js.map +1 -1
- package/dist/wire/src/model.d.ts +387 -6
- package/dist/wire/src/model.js +74 -0
- package/dist/wire/src/model.js.map +1 -1
- package/dist/wire/src/relay.d.ts +8 -8
- package/dist/wire/src/rpc.d.ts +2524 -200
- package/dist/wire/src/rpc.js +77 -4
- package/dist/wire/src/rpc.js.map +1 -1
- package/dist/wire/src/updates.d.ts +390 -34
- package/dist/wire/src/updates.js +11 -1
- package/dist/wire/src/updates.js.map +1 -1
- package/package.json +12 -12
- package/src/accounts.ts +32 -0
- package/src/index.ts +4 -997
- package/src/main.ts +65 -0
- package/src/rate-limit.ts +72 -0
- package/src/relay.ts +435 -620
- package/src/serve.ts +130 -0
- package/src/store.ts +135 -0
- package/src/subscription.ts +51 -0
- package/dist/gateway/src/auth-middleware.d.ts +0 -44
- package/dist/gateway/src/auth-middleware.js +0 -204
- package/dist/gateway/src/auth-middleware.js.map +0 -1
- package/dist/gateway/src/cors.d.ts +0 -2
- package/dist/gateway/src/cors.js +0 -7
- package/dist/gateway/src/cors.js.map +0 -1
- package/dist/gateway/src/embedded.d.ts +0 -21
- package/dist/gateway/src/embedded.js +0 -438
- package/dist/gateway/src/embedded.js.map +0 -1
- package/dist/gateway/src/host-auth.d.ts +0 -32
- package/dist/gateway/src/host-auth.js +0 -78
- package/dist/gateway/src/host-auth.js.map +0 -1
- package/dist/gateway/src/pairings.d.ts +0 -38
- package/dist/gateway/src/pairings.js +0 -119
- package/dist/gateway/src/pairings.js.map +0 -1
- package/dist/gateway/src/presence.d.ts +0 -28
- package/dist/gateway/src/presence.js +0 -92
- package/dist/gateway/src/presence.js.map +0 -1
- package/dist/gateway/src/sessions.d.ts +0 -113
- package/dist/gateway/src/sessions.js +0 -352
- package/dist/gateway/src/sessions.js.map +0 -1
- package/dist/gateway/src/state-store.d.ts +0 -21
- package/dist/gateway/src/state-store.js +0 -103
- package/dist/gateway/src/state-store.js.map +0 -1
- package/dist/gateway/src/static-web.d.ts +0 -15
- package/dist/gateway/src/static-web.js +0 -129
- package/dist/gateway/src/static-web.js.map +0 -1
- package/dist/gateway/src/tokens.d.ts +0 -19
- package/dist/gateway/src/tokens.js +0 -151
- package/dist/gateway/src/tokens.js.map +0 -1
- package/dist/gateway/src/tunnel.d.ts +0 -61
- package/dist/gateway/src/tunnel.js +0 -576
- package/dist/gateway/src/tunnel.js.map +0 -1
- package/dist/gateway-v2/src/accounts.js.map +0 -1
- package/dist/gateway-v2/src/index.d.ts +0 -3
- package/dist/gateway-v2/src/index.js +0 -4
- package/dist/gateway-v2/src/index.js.map +0 -1
- package/dist/gateway-v2/src/main.js +0 -17
- package/dist/gateway-v2/src/main.js.map +0 -1
- package/dist/gateway-v2/src/server.d.ts +0 -84
- package/dist/gateway-v2/src/server.js +0 -426
- package/dist/gateway-v2/src/server.js.map +0 -1
- package/dist/gateway-v2/src/store.js.map +0 -1
- package/dist/shared-protocol/src/index.d.ts +0 -17029
- package/dist/shared-protocol/src/index.js +0 -1064
- package/dist/shared-protocol/src/index.js.map +0 -1
- package/src/auth-middleware.ts +0 -266
- package/src/cors.ts +0 -8
- package/src/embedded.ts +0 -526
- package/src/host-auth.ts +0 -88
- package/src/pairings.ts +0 -133
- package/src/presence.ts +0 -118
- package/src/sessions.ts +0 -432
- package/src/state-store.ts +0 -140
- package/src/static-web.ts +0 -133
- package/src/tokens.ts +0 -159
- package/src/tunnel.ts +0 -644
- /package/dist/{gateway-v2 → gateway}/src/accounts.d.ts +0 -0
- /package/dist/{gateway-v2 → gateway}/src/accounts.js +0 -0
- /package/dist/{gateway-v2 → gateway}/src/main.d.ts +0 -0
package/src/main.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { supabaseVerifier } from "./accounts.js";
|
|
2
|
+
import { packageVersion, startGateway } from "./serve.js";
|
|
3
|
+
import { requirePro, supabaseSubscriptions } from "./subscription.js";
|
|
4
|
+
|
|
5
|
+
// The gateway as a process (the Docker image runs this), configured by environment:
|
|
6
|
+
// PORT listen port (8787)
|
|
7
|
+
// RELAY_DATA_PATH SQLite file with pairings and keys (./data/relay.db); keep it on a volume
|
|
8
|
+
// SUPABASE_URL + SUPABASE_ANON_KEY turn accounts on
|
|
9
|
+
// AUTH_REQUIRED=true computers need an active Pro account (with SUPABASE_SERVICE_ROLE_KEY)
|
|
10
|
+
// TRUSTED_PROXIES comma-separated proxy addresses whose X-Forwarded-For is believed
|
|
11
|
+
// WS_CONNECT_RATE_LIMIT_MAX / WS_CONNECT_RATE_LIMIT_WINDOW_MS connections per address (20 per 60000 ms)
|
|
12
|
+
// LOG_LEVEL debug | info | warn | error (info)
|
|
13
|
+
|
|
14
|
+
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
15
|
+
type LogLevel = keyof typeof LOG_LEVELS;
|
|
16
|
+
const logLevel = (process.env.LOG_LEVEL ?? "info") as LogLevel;
|
|
17
|
+
|
|
18
|
+
function log(level: LogLevel, message: string): void {
|
|
19
|
+
if (LOG_LEVELS[level] >= LOG_LEVELS[logLevel]) process.stdout.write(`[gateway:${level}] ${message}\n`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const port = Number(process.env.PORT ?? 8787);
|
|
23
|
+
const databasePath = process.env.RELAY_DATA_PATH ?? "./data/relay.db";
|
|
24
|
+
const supabase = {
|
|
25
|
+
url: process.env.SUPABASE_URL ?? "",
|
|
26
|
+
anonKey: process.env.SUPABASE_ANON_KEY ?? "",
|
|
27
|
+
serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY ?? "",
|
|
28
|
+
};
|
|
29
|
+
const version = packageVersion();
|
|
30
|
+
|
|
31
|
+
const gateway = await startGateway({
|
|
32
|
+
port,
|
|
33
|
+
databasePath,
|
|
34
|
+
version,
|
|
35
|
+
log: (message) => log("info", `[relay] ${message}`),
|
|
36
|
+
verifyToken: supabase.url && supabase.anonKey ? supabaseVerifier(supabase.url, supabase.anonKey) : undefined,
|
|
37
|
+
admit:
|
|
38
|
+
process.env.AUTH_REQUIRED === "true"
|
|
39
|
+
? requirePro(supabaseSubscriptions(supabase), (message) => log("warn", `[relay] ${message}`))
|
|
40
|
+
: undefined,
|
|
41
|
+
trustedProxies: (process.env.TRUSTED_PROXIES ?? "").split(","),
|
|
42
|
+
wsConnectLimit: {
|
|
43
|
+
max: Number(process.env.WS_CONNECT_RATE_LIMIT_MAX ?? 20),
|
|
44
|
+
windowMs: Number(process.env.WS_CONNECT_RATE_LIMIT_WINDOW_MS ?? 60_000),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
log("info", `LinkShell Gateway v${version}`);
|
|
49
|
+
log("info", `listening on http://0.0.0.0:${gateway.port}`);
|
|
50
|
+
// Said at every start: coming up on an empty file is how every phone gets unpaired.
|
|
51
|
+
log("info", `pairings and keys in ${databasePath}`);
|
|
52
|
+
log("info", `log level: ${logLevel}`);
|
|
53
|
+
|
|
54
|
+
function shutdown(): void {
|
|
55
|
+
process.stdout.write("[gateway] shutting down...\n");
|
|
56
|
+
void gateway.close().then(() => {
|
|
57
|
+
process.stdout.write("[gateway] stopped\n");
|
|
58
|
+
process.exit(0);
|
|
59
|
+
});
|
|
60
|
+
// Connections that won't close don't get to hold the process.
|
|
61
|
+
setTimeout(() => process.exit(1), 5000).unref();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
process.on("SIGINT", shutdown);
|
|
65
|
+
process.on("SIGTERM", shutdown);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
|
|
3
|
+
/** Counts hits per key in fixed windows; over `maxHits` in one window is refused. */
|
|
4
|
+
export class RateLimiter {
|
|
5
|
+
private readonly hits = new Map<string, { count: number; resetAt: number }>();
|
|
6
|
+
private readonly pruneTimer: ReturnType<typeof setInterval>;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
private readonly maxHits: number,
|
|
10
|
+
private readonly windowMs: number,
|
|
11
|
+
) {
|
|
12
|
+
// Finished windows are dropped, so the map holds only keys seen lately.
|
|
13
|
+
this.pruneTimer = setInterval(() => this.prune(), Math.max(windowMs, 60_000));
|
|
14
|
+
this.pruneTimer.unref();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
allow(key: string): boolean {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const entry = this.hits.get(key);
|
|
20
|
+
if (!entry || now >= entry.resetAt) {
|
|
21
|
+
this.hits.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
entry.count++;
|
|
25
|
+
return entry.count <= this.maxHits;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private prune(): void {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
for (const [key, entry] of this.hits) {
|
|
31
|
+
if (now >= entry.resetAt) this.hits.delete(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
destroy(): void {
|
|
36
|
+
clearInterval(this.pruneTimer);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A host or a test on this machine: never rate limited. */
|
|
41
|
+
export function isLoopback(ip: string): boolean {
|
|
42
|
+
const normalized = ip.trim().toLowerCase();
|
|
43
|
+
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "::ffff:127.0.0.1";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** IPv4 addresses arrive IPv6-mapped (::ffff:1.2.3.4) on a dual-stack socket. */
|
|
47
|
+
function normalizeIp(ip: string): string {
|
|
48
|
+
const trimmed = ip.trim().toLowerCase();
|
|
49
|
+
return trimmed.startsWith("::ffff:") ? trimmed.slice(7) : trimmed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The reverse proxies in front of this gateway, from a list of their addresses. */
|
|
53
|
+
export function trustedProxySet(addresses: readonly string[]): Set<string> {
|
|
54
|
+
return new Set(addresses.map(normalizeIp).filter(Boolean));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Who is connecting. X-Forwarded-For is believed only when the direct peer is
|
|
59
|
+
* one of `trustedProxies`: from anyone else the header is the caller's own
|
|
60
|
+
* claim, and believing it would let them pick their rate-limit key.
|
|
61
|
+
*/
|
|
62
|
+
export function clientIp(request: IncomingMessage, trustedProxies: ReadonlySet<string>): string {
|
|
63
|
+
const peer = request.socket.remoteAddress ?? "unknown";
|
|
64
|
+
if (trustedProxies.has(normalizeIp(peer))) {
|
|
65
|
+
const forwarded = request.headers["x-forwarded-for"];
|
|
66
|
+
if (typeof forwarded === "string") {
|
|
67
|
+
const first = forwarded.split(",")[0]?.trim();
|
|
68
|
+
if (first) return first;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return peer;
|
|
72
|
+
}
|