@linkshell/gateway 0.5.3 → 0.6.1

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.
Files changed (109) hide show
  1. package/Dockerfile +8 -22
  2. package/README.md +102 -44
  3. package/dist/gateway/src/accounts.js.map +1 -0
  4. package/dist/gateway/src/index.d.ts +4 -1
  5. package/dist/gateway/src/index.js +4 -863
  6. package/dist/gateway/src/index.js.map +1 -1
  7. package/dist/gateway/src/main.js +57 -0
  8. package/dist/gateway/src/main.js.map +1 -0
  9. package/dist/gateway/src/rate-limit.d.ts +32 -0
  10. package/dist/gateway/src/rate-limit.js +106 -0
  11. package/dist/gateway/src/rate-limit.js.map +1 -0
  12. package/dist/gateway/src/relay.d.ts +84 -5
  13. package/dist/gateway/src/relay.js +402 -529
  14. package/dist/gateway/src/relay.js.map +1 -1
  15. package/dist/gateway/src/serve.d.ts +34 -0
  16. package/dist/gateway/src/serve.js +115 -0
  17. package/dist/gateway/src/serve.js.map +1 -0
  18. package/dist/{gateway-v2 → gateway}/src/store.d.ts +5 -0
  19. package/dist/{gateway-v2 → gateway}/src/store.js +7 -0
  20. package/dist/gateway/src/store.js.map +1 -0
  21. package/dist/gateway/src/subscription.d.ts +18 -0
  22. package/dist/gateway/src/subscription.js +46 -0
  23. package/dist/gateway/src/subscription.js.map +1 -0
  24. package/dist/gateway/tsconfig.tsbuildinfo +1 -1
  25. package/dist/wire/src/direct.d.ts +22 -0
  26. package/dist/wire/src/direct.js +37 -0
  27. package/dist/wire/src/direct.js.map +1 -0
  28. package/dist/wire/src/index.d.ts +1 -0
  29. package/dist/wire/src/index.js +1 -0
  30. package/dist/wire/src/index.js.map +1 -1
  31. package/dist/wire/src/model.d.ts +287 -6
  32. package/dist/wire/src/model.js +33 -0
  33. package/dist/wire/src/model.js.map +1 -1
  34. package/dist/wire/src/relay.d.ts +8 -8
  35. package/dist/wire/src/rpc.d.ts +1780 -44
  36. package/dist/wire/src/rpc.js +48 -3
  37. package/dist/wire/src/rpc.js.map +1 -1
  38. package/dist/wire/src/updates.d.ts +356 -0
  39. package/dist/wire/src/updates.js +11 -1
  40. package/dist/wire/src/updates.js.map +1 -1
  41. package/package.json +13 -13
  42. package/src/accounts.ts +32 -0
  43. package/src/index.ts +4 -997
  44. package/src/main.ts +65 -0
  45. package/src/rate-limit.ts +114 -0
  46. package/src/relay.ts +435 -620
  47. package/src/serve.ts +142 -0
  48. package/src/store.ts +135 -0
  49. package/src/subscription.ts +51 -0
  50. package/dist/gateway/src/auth-middleware.d.ts +0 -44
  51. package/dist/gateway/src/auth-middleware.js +0 -204
  52. package/dist/gateway/src/auth-middleware.js.map +0 -1
  53. package/dist/gateway/src/cors.d.ts +0 -2
  54. package/dist/gateway/src/cors.js +0 -7
  55. package/dist/gateway/src/cors.js.map +0 -1
  56. package/dist/gateway/src/embedded.d.ts +0 -21
  57. package/dist/gateway/src/embedded.js +0 -438
  58. package/dist/gateway/src/embedded.js.map +0 -1
  59. package/dist/gateway/src/host-auth.d.ts +0 -32
  60. package/dist/gateway/src/host-auth.js +0 -78
  61. package/dist/gateway/src/host-auth.js.map +0 -1
  62. package/dist/gateway/src/pairings.d.ts +0 -38
  63. package/dist/gateway/src/pairings.js +0 -119
  64. package/dist/gateway/src/pairings.js.map +0 -1
  65. package/dist/gateway/src/presence.d.ts +0 -28
  66. package/dist/gateway/src/presence.js +0 -92
  67. package/dist/gateway/src/presence.js.map +0 -1
  68. package/dist/gateway/src/sessions.d.ts +0 -113
  69. package/dist/gateway/src/sessions.js +0 -352
  70. package/dist/gateway/src/sessions.js.map +0 -1
  71. package/dist/gateway/src/state-store.d.ts +0 -21
  72. package/dist/gateway/src/state-store.js +0 -103
  73. package/dist/gateway/src/state-store.js.map +0 -1
  74. package/dist/gateway/src/static-web.d.ts +0 -15
  75. package/dist/gateway/src/static-web.js +0 -129
  76. package/dist/gateway/src/static-web.js.map +0 -1
  77. package/dist/gateway/src/tokens.d.ts +0 -19
  78. package/dist/gateway/src/tokens.js +0 -151
  79. package/dist/gateway/src/tokens.js.map +0 -1
  80. package/dist/gateway/src/tunnel.d.ts +0 -61
  81. package/dist/gateway/src/tunnel.js +0 -576
  82. package/dist/gateway/src/tunnel.js.map +0 -1
  83. package/dist/gateway-v2/src/accounts.js.map +0 -1
  84. package/dist/gateway-v2/src/index.d.ts +0 -3
  85. package/dist/gateway-v2/src/index.js +0 -4
  86. package/dist/gateway-v2/src/index.js.map +0 -1
  87. package/dist/gateway-v2/src/main.js +0 -17
  88. package/dist/gateway-v2/src/main.js.map +0 -1
  89. package/dist/gateway-v2/src/server.d.ts +0 -84
  90. package/dist/gateway-v2/src/server.js +0 -426
  91. package/dist/gateway-v2/src/server.js.map +0 -1
  92. package/dist/gateway-v2/src/store.js.map +0 -1
  93. package/dist/shared-protocol/src/index.d.ts +0 -17029
  94. package/dist/shared-protocol/src/index.js +0 -1064
  95. package/dist/shared-protocol/src/index.js.map +0 -1
  96. package/src/auth-middleware.ts +0 -266
  97. package/src/cors.ts +0 -8
  98. package/src/embedded.ts +0 -526
  99. package/src/host-auth.ts +0 -88
  100. package/src/pairings.ts +0 -133
  101. package/src/presence.ts +0 -118
  102. package/src/sessions.ts +0 -432
  103. package/src/state-store.ts +0 -140
  104. package/src/static-web.ts +0 -133
  105. package/src/tokens.ts +0 -159
  106. package/src/tunnel.ts +0 -644
  107. /package/dist/{gateway-v2 → gateway}/src/accounts.d.ts +0 -0
  108. /package/dist/{gateway-v2 → gateway}/src/accounts.js +0 -0
  109. /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 (or IPv4 ranges, 10.0.0.0/8) 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,114 @@
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. */
53
+ export interface TrustedProxies {
54
+ has(ip: string): boolean;
55
+ }
56
+
57
+ function ipv4Number(ip: string): number | undefined {
58
+ const parts = ip.split(".");
59
+ if (parts.length !== 4) return undefined;
60
+ let value = 0;
61
+ for (const part of parts) {
62
+ if (!/^\d{1,3}$/.test(part) || Number(part) > 255) return undefined;
63
+ value = value * 256 + Number(part);
64
+ }
65
+ return value;
66
+ }
67
+
68
+ /**
69
+ * From a list of their addresses. An IPv4 range can be given as `10.0.0.0/8`: a proxy in a container network
70
+ * has another address after every deploy, and only its network is known beforehand.
71
+ */
72
+ export function trustedProxySet(addresses: readonly string[]): TrustedProxies {
73
+ const exact = new Set<string>();
74
+ const ranges: { base: number; size: number }[] = [];
75
+ for (const address of addresses.map(normalizeIp).filter(Boolean)) {
76
+ const [network, bits, ...rest] = address.split("/");
77
+ const base = bits === undefined ? undefined : ipv4Number(network ?? "");
78
+ const prefix = Number(bits);
79
+ if (base === undefined || rest.length > 0 || !/^\d{1,2}$/.test(bits ?? "") || prefix > 32) {
80
+ exact.add(address);
81
+ continue;
82
+ }
83
+ const size = 2 ** (32 - prefix);
84
+ ranges.push({ base: base - (base % size), size });
85
+ }
86
+ return {
87
+ has(ip) {
88
+ const address = normalizeIp(ip);
89
+ if (exact.has(address)) return true;
90
+ const value = ranges.length > 0 ? ipv4Number(address) : undefined;
91
+ return value !== undefined && ranges.some((range) => value >= range.base && value < range.base + range.size);
92
+ },
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Who is connecting. X-Forwarded-For is believed only when the direct peer is
98
+ * one of `trustedProxies`: from anyone else the header is the caller's own
99
+ * claim, and believing it would let them pick their rate-limit key. The same
100
+ * goes for what the header starts with: a proxy adds the address it saw to
101
+ * the end of whatever the caller sent, so the client is the last address that
102
+ * isn't a proxy's, not the first one written.
103
+ */
104
+ export function clientIp(request: IncomingMessage, trustedProxies: TrustedProxies): string {
105
+ const peer = request.socket.remoteAddress ?? "unknown";
106
+ if (!trustedProxies.has(peer)) return peer;
107
+ const forwarded = request.headers["x-forwarded-for"];
108
+ const hops = (Array.isArray(forwarded) ? forwarded.join(",") : (forwarded ?? "")).split(",").map((hop) => hop.trim()).filter(Boolean);
109
+ for (let i = hops.length - 1; i >= 0; i--) {
110
+ if (!trustedProxies.has(hops[i]!)) return hops[i]!;
111
+ }
112
+ // Every hop is a proxy of ours (or there is no header): the connection is the proxy's own.
113
+ return hops[0] ?? peer;
114
+ }