linkshell-cli 0.9.0 → 0.10.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.
Files changed (153) hide show
  1. package/README.md +11 -6
  2. package/dist/cli/src/commands/gateway.d.ts +2 -0
  3. package/dist/cli/src/commands/gateway.js +29 -0
  4. package/dist/cli/src/commands/gateway.js.map +1 -0
  5. package/dist/cli/src/commands/host.d.ts +2 -0
  6. package/dist/cli/src/commands/host.js +12 -2
  7. package/dist/cli/src/commands/host.js.map +1 -1
  8. package/dist/cli/src/commands/setup.js +4 -2
  9. package/dist/cli/src/commands/setup.js.map +1 -1
  10. package/dist/cli/src/index.js +12 -334
  11. package/dist/cli/src/index.js.map +1 -1
  12. package/dist/cli/src/utils/daemon.d.ts +0 -7
  13. package/dist/cli/src/utils/daemon.js +1 -18
  14. package/dist/cli/src/utils/daemon.js.map +1 -1
  15. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  16. package/dist/gateway/src/accounts.d.ts +6 -0
  17. package/dist/gateway/src/accounts.js +32 -0
  18. package/dist/gateway/src/accounts.js.map +1 -0
  19. package/dist/gateway/src/index.d.ts +4 -0
  20. package/dist/gateway/src/index.js +5 -0
  21. package/dist/gateway/src/index.js.map +1 -0
  22. package/dist/gateway/src/main.d.ts +1 -0
  23. package/dist/gateway/src/main.js +57 -0
  24. package/dist/gateway/src/main.js.map +1 -0
  25. package/dist/gateway/src/rate-limit.d.ts +22 -0
  26. package/dist/gateway/src/rate-limit.js +66 -0
  27. package/dist/gateway/src/rate-limit.js.map +1 -0
  28. package/dist/gateway/src/relay.d.ts +84 -0
  29. package/dist/gateway/src/relay.js +434 -0
  30. package/dist/gateway/src/relay.js.map +1 -0
  31. package/dist/gateway/src/serve.d.ts +34 -0
  32. package/dist/gateway/src/serve.js +102 -0
  33. package/dist/gateway/src/serve.js.map +1 -0
  34. package/dist/gateway/src/store.d.ts +35 -0
  35. package/dist/gateway/src/store.js +109 -0
  36. package/dist/gateway/src/store.js.map +1 -0
  37. package/dist/gateway/src/subscription.d.ts +18 -0
  38. package/dist/gateway/src/subscription.js +46 -0
  39. package/dist/gateway/src/subscription.js.map +1 -0
  40. package/dist/host/src/drivers/claude/transcript.d.ts +23 -23
  41. package/dist/host/src/fs.d.ts +3 -3
  42. package/dist/host/src/input.d.ts +4 -4
  43. package/dist/host/src/terminals.d.ts +1 -1
  44. package/dist/wire/src/model.d.ts +6 -6
  45. package/dist/wire/src/relay.d.ts +8 -8
  46. package/dist/wire/src/rpc.d.ts +818 -818
  47. package/dist/wire/src/updates.d.ts +18 -18
  48. package/package.json +4 -13
  49. package/src/commands/gateway.ts +31 -0
  50. package/src/commands/host.ts +13 -2
  51. package/src/commands/setup.ts +4 -2
  52. package/src/index.ts +14 -429
  53. package/src/utils/daemon.ts +2 -27
  54. package/dist/cli/src/config.d.ts +0 -14
  55. package/dist/cli/src/config.js +0 -23
  56. package/dist/cli/src/config.js.map +0 -1
  57. package/dist/cli/src/machine-id.d.ts +0 -6
  58. package/dist/cli/src/machine-id.js +0 -50
  59. package/dist/cli/src/machine-id.js.map +0 -1
  60. package/dist/cli/src/providers.d.ts +0 -13
  61. package/dist/cli/src/providers.js +0 -77
  62. package/dist/cli/src/providers.js.map +0 -1
  63. package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -128
  64. package/dist/cli/src/runtime/acp/acp-client.js +0 -430
  65. package/dist/cli/src/runtime/acp/acp-client.js.map +0 -1
  66. package/dist/cli/src/runtime/acp/agent-process-discovery.d.ts +0 -48
  67. package/dist/cli/src/runtime/acp/agent-process-discovery.js +0 -278
  68. package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +0 -1
  69. package/dist/cli/src/runtime/acp/agent-session.d.ts +0 -62
  70. package/dist/cli/src/runtime/acp/agent-session.js +0 -1081
  71. package/dist/cli/src/runtime/acp/agent-session.js.map +0 -1
  72. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +0 -311
  73. package/dist/cli/src/runtime/acp/agent-workspace.js +0 -6083
  74. package/dist/cli/src/runtime/acp/agent-workspace.js.map +0 -1
  75. package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +0 -121
  76. package/dist/cli/src/runtime/acp/claude-sdk-client.js +0 -1147
  77. package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +0 -1
  78. package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +0 -53
  79. package/dist/cli/src/runtime/acp/claude-stream-json-client.js +0 -552
  80. package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +0 -1
  81. package/dist/cli/src/runtime/acp/json-rpc.d.ts +0 -22
  82. package/dist/cli/src/runtime/acp/json-rpc.js +0 -160
  83. package/dist/cli/src/runtime/acp/json-rpc.js.map +0 -1
  84. package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -16
  85. package/dist/cli/src/runtime/acp/provider-resolver.js +0 -132
  86. package/dist/cli/src/runtime/acp/provider-resolver.js.map +0 -1
  87. package/dist/cli/src/runtime/bridge-session.d.ts +0 -97
  88. package/dist/cli/src/runtime/bridge-session.js +0 -1364
  89. package/dist/cli/src/runtime/bridge-session.js.map +0 -1
  90. package/dist/cli/src/runtime/hook-command.d.ts +0 -9
  91. package/dist/cli/src/runtime/hook-command.js +0 -80
  92. package/dist/cli/src/runtime/hook-command.js.map +0 -1
  93. package/dist/cli/src/runtime/screen-fallback.d.ts +0 -22
  94. package/dist/cli/src/runtime/screen-fallback.js +0 -202
  95. package/dist/cli/src/runtime/screen-fallback.js.map +0 -1
  96. package/dist/cli/src/runtime/screen-share.d.ts +0 -34
  97. package/dist/cli/src/runtime/screen-share.js +0 -376
  98. package/dist/cli/src/runtime/screen-share.js.map +0 -1
  99. package/dist/cli/src/runtime/scrollback.d.ts +0 -11
  100. package/dist/cli/src/runtime/scrollback.js +0 -33
  101. package/dist/cli/src/runtime/scrollback.js.map +0 -1
  102. package/dist/cli/src/runtime/usage-report.d.ts +0 -34
  103. package/dist/cli/src/runtime/usage-report.js +0 -332
  104. package/dist/cli/src/runtime/usage-report.js.map +0 -1
  105. package/dist/cli/src/utils/keep-awake.d.ts +0 -6
  106. package/dist/cli/src/utils/keep-awake.js +0 -48
  107. package/dist/cli/src/utils/keep-awake.js.map +0 -1
  108. package/dist/cli/src/utils/lan-ip.d.ts +0 -9
  109. package/dist/cli/src/utils/lan-ip.js +0 -61
  110. package/dist/cli/src/utils/lan-ip.js.map +0 -1
  111. package/dist/host/src/input-helper.d.ts +0 -1
  112. package/dist/host/src/input-helper.js +0 -564
  113. package/dist/host/src/input-helper.js.map +0 -1
  114. package/dist/shared-protocol/src/index.d.ts +0 -17029
  115. package/dist/shared-protocol/src/index.js +0 -1064
  116. package/dist/shared-protocol/src/index.js.map +0 -1
  117. package/scripts/copy-web.mjs +0 -34
  118. package/src/config.ts +0 -36
  119. package/src/machine-id.ts +0 -61
  120. package/src/providers.ts +0 -97
  121. package/src/runtime/acp/acp-client.ts +0 -521
  122. package/src/runtime/acp/agent-process-discovery.ts +0 -308
  123. package/src/runtime/acp/agent-session.ts +0 -1186
  124. package/src/runtime/acp/agent-workspace.ts +0 -6590
  125. package/src/runtime/acp/claude-sdk-client.ts +0 -1221
  126. package/src/runtime/acp/claude-stream-json-client.ts +0 -633
  127. package/src/runtime/acp/json-rpc.ts +0 -184
  128. package/src/runtime/acp/provider-resolver.ts +0 -154
  129. package/src/runtime/bridge-session.ts +0 -1549
  130. package/src/runtime/hook-command.ts +0 -73
  131. package/src/runtime/screen-fallback.ts +0 -235
  132. package/src/runtime/screen-share.ts +0 -426
  133. package/src/runtime/scrollback.ts +0 -43
  134. package/src/runtime/usage-report.ts +0 -342
  135. package/src/utils/keep-awake.ts +0 -61
  136. package/src/utils/lan-ip.ts +0 -61
  137. package/web/assets/FileBrowser-wOAfY-xj.js +0 -2
  138. package/web/assets/PortPreview-Cbvv7yzA.js +0 -2
  139. package/web/assets/TerminalPanel-CTSklkct.js +0 -1
  140. package/web/assets/UsageDashboard-CJ1AsNmd.js +0 -1
  141. package/web/assets/index-BsnDaeCt.css +0 -33
  142. package/web/assets/index-q85psYZ5.js +0 -36
  143. package/web/assets/vendor-highlight-NXWAAyIv.js +0 -5
  144. package/web/assets/vendor-react-M7xG3qiw.js +0 -49
  145. package/web/assets/vendor-xterm-D15THcI4.js +0 -9
  146. package/web/index.html +0 -37
  147. package/web/logos/claude.png +0 -0
  148. package/web/logos/codex.png +0 -0
  149. package/web/logos/copilot.png +0 -0
  150. package/web/logos/gemini.png +0 -0
  151. package/web/logos/linkshell-1024.png +0 -0
  152. package/web/logos/linkshell-splash.png +0 -0
  153. package/web/logos/linkshell.png +0 -0
@@ -0,0 +1,6 @@
1
+ export interface Account {
2
+ userId: string;
3
+ email?: string;
4
+ }
5
+ export type VerifyToken = (token: string) => Promise<Account | undefined>;
6
+ export declare function supabaseVerifier(url: string, anonKey: string): VerifyToken;
@@ -0,0 +1,32 @@
1
+ // Account tokens (Supabase JWTs from the iTool login). The gateway only needs
2
+ // to know whose token it is; it never sees passwords.
3
+ export function supabaseVerifier(url, anonKey) {
4
+ const cache = new Map();
5
+ return async (token) => {
6
+ const hit = cache.get(token);
7
+ if (hit && hit.until > Date.now())
8
+ return hit.account;
9
+ try {
10
+ const response = await fetch(`${url}/auth/v1/user`, {
11
+ headers: { Authorization: `Bearer ${token}`, apikey: anonKey },
12
+ signal: AbortSignal.timeout(5000),
13
+ });
14
+ if (!response.ok)
15
+ return undefined;
16
+ const user = (await response.json());
17
+ if (!user.id)
18
+ return undefined;
19
+ const account = { userId: user.id, email: user.email };
20
+ cache.set(token, { account, until: Date.now() + 60_000 });
21
+ if (cache.size > 5000)
22
+ for (const [key, value] of cache)
23
+ if (value.until < Date.now())
24
+ cache.delete(key);
25
+ return account;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ };
31
+ }
32
+ //# sourceMappingURL=accounts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accounts.js","sourceRoot":"","sources":["../../../../gateway/src/accounts.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,sDAAsD;AAStD,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,OAAe;IAC3D,MAAM,KAAK,GAAG,IAAI,GAAG,EAA+C,CAAC;IACrE,OAAO,KAAK,EAAE,KAAK,EAAE,EAAE;QACrB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC7B,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE;YAAE,OAAO,GAAG,CAAC,OAAO,CAAC;QACtD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,GAAG,eAAe,EAAE;gBAClD,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;gBAC9D,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;aAClC,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,OAAO,SAAS,CAAC;YACnC,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAoC,CAAC;YACxE,IAAI,CAAC,IAAI,CAAC,EAAE;gBAAE,OAAO,SAAS,CAAC;YAC/B,MAAM,OAAO,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;YACvD,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;YAC1D,IAAI,KAAK,CAAC,IAAI,GAAG,IAAI;gBAAE,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,KAAK;oBAAE,IAAI,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE;wBAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACzG,OAAO,OAAO,CAAC;QACjB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { Gateway, type GatewayOptions } from "./relay.js";
2
+ export { GatewayStore } from "./store.js";
3
+ export { supabaseVerifier, type Account, type VerifyToken } from "./accounts.js";
4
+ export { startGateway, type RunningGateway, type StartGatewayOptions } from "./serve.js";
@@ -0,0 +1,5 @@
1
+ export { Gateway } from "./relay.js";
2
+ export { GatewayStore } from "./store.js";
3
+ export { supabaseVerifier } from "./accounts.js";
4
+ export { startGateway } from "./serve.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../gateway/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAuB,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAkC,MAAM,eAAe,CAAC;AACjF,OAAO,EAAE,YAAY,EAAiD,MAAM,YAAY,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ import { supabaseVerifier } from "./accounts.js";
2
+ import { packageVersion, startGateway } from "./serve.js";
3
+ import { requirePro, supabaseSubscriptions } from "./subscription.js";
4
+ // The gateway as a process (the Docker image runs this), configured by environment:
5
+ // PORT listen port (8787)
6
+ // RELAY_DATA_PATH SQLite file with pairings and keys (./data/relay.db); keep it on a volume
7
+ // SUPABASE_URL + SUPABASE_ANON_KEY turn accounts on
8
+ // AUTH_REQUIRED=true computers need an active Pro account (with SUPABASE_SERVICE_ROLE_KEY)
9
+ // TRUSTED_PROXIES comma-separated proxy addresses whose X-Forwarded-For is believed
10
+ // WS_CONNECT_RATE_LIMIT_MAX / WS_CONNECT_RATE_LIMIT_WINDOW_MS connections per address (20 per 60000 ms)
11
+ // LOG_LEVEL debug | info | warn | error (info)
12
+ const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
13
+ const logLevel = (process.env.LOG_LEVEL ?? "info");
14
+ function log(level, message) {
15
+ if (LOG_LEVELS[level] >= LOG_LEVELS[logLevel])
16
+ process.stdout.write(`[gateway:${level}] ${message}\n`);
17
+ }
18
+ const port = Number(process.env.PORT ?? 8787);
19
+ const databasePath = process.env.RELAY_DATA_PATH ?? "./data/relay.db";
20
+ const supabase = {
21
+ url: process.env.SUPABASE_URL ?? "",
22
+ anonKey: process.env.SUPABASE_ANON_KEY ?? "",
23
+ serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY ?? "",
24
+ };
25
+ const version = packageVersion();
26
+ const gateway = await startGateway({
27
+ port,
28
+ databasePath,
29
+ version,
30
+ log: (message) => log("info", `[relay] ${message}`),
31
+ verifyToken: supabase.url && supabase.anonKey ? supabaseVerifier(supabase.url, supabase.anonKey) : undefined,
32
+ admit: process.env.AUTH_REQUIRED === "true"
33
+ ? requirePro(supabaseSubscriptions(supabase), (message) => log("warn", `[relay] ${message}`))
34
+ : undefined,
35
+ trustedProxies: (process.env.TRUSTED_PROXIES ?? "").split(","),
36
+ wsConnectLimit: {
37
+ max: Number(process.env.WS_CONNECT_RATE_LIMIT_MAX ?? 20),
38
+ windowMs: Number(process.env.WS_CONNECT_RATE_LIMIT_WINDOW_MS ?? 60_000),
39
+ },
40
+ });
41
+ log("info", `LinkShell Gateway v${version}`);
42
+ log("info", `listening on http://0.0.0.0:${gateway.port}`);
43
+ // Said at every start: coming up on an empty file is how every phone gets unpaired.
44
+ log("info", `pairings and keys in ${databasePath}`);
45
+ log("info", `log level: ${logLevel}`);
46
+ function shutdown() {
47
+ process.stdout.write("[gateway] shutting down...\n");
48
+ void gateway.close().then(() => {
49
+ process.stdout.write("[gateway] stopped\n");
50
+ process.exit(0);
51
+ });
52
+ // Connections that won't close don't get to hold the process.
53
+ setTimeout(() => process.exit(1), 5000).unref();
54
+ }
55
+ process.on("SIGINT", shutdown);
56
+ process.on("SIGTERM", shutdown);
57
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../../../gateway/src/main.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ /** Counts hits per key in fixed windows; over `maxHits` in one window is refused. */
3
+ export declare class RateLimiter {
4
+ private readonly maxHits;
5
+ private readonly windowMs;
6
+ private readonly hits;
7
+ private readonly pruneTimer;
8
+ constructor(maxHits: number, windowMs: number);
9
+ allow(key: string): boolean;
10
+ private prune;
11
+ destroy(): void;
12
+ }
13
+ /** A host or a test on this machine: never rate limited. */
14
+ export declare function isLoopback(ip: string): boolean;
15
+ /** The reverse proxies in front of this gateway, from a list of their addresses. */
16
+ export declare function trustedProxySet(addresses: readonly string[]): Set<string>;
17
+ /**
18
+ * Who is connecting. X-Forwarded-For is believed only when the direct peer is
19
+ * one of `trustedProxies`: from anyone else the header is the caller's own
20
+ * claim, and believing it would let them pick their rate-limit key.
21
+ */
22
+ export declare function clientIp(request: IncomingMessage, trustedProxies: ReadonlySet<string>): string;
@@ -0,0 +1,66 @@
1
+ /** Counts hits per key in fixed windows; over `maxHits` in one window is refused. */
2
+ export class RateLimiter {
3
+ maxHits;
4
+ windowMs;
5
+ hits = new Map();
6
+ pruneTimer;
7
+ constructor(maxHits, windowMs) {
8
+ this.maxHits = maxHits;
9
+ this.windowMs = windowMs;
10
+ // Finished windows are dropped, so the map holds only keys seen lately.
11
+ this.pruneTimer = setInterval(() => this.prune(), Math.max(windowMs, 60_000));
12
+ this.pruneTimer.unref();
13
+ }
14
+ allow(key) {
15
+ const now = Date.now();
16
+ const entry = this.hits.get(key);
17
+ if (!entry || now >= entry.resetAt) {
18
+ this.hits.set(key, { count: 1, resetAt: now + this.windowMs });
19
+ return true;
20
+ }
21
+ entry.count++;
22
+ return entry.count <= this.maxHits;
23
+ }
24
+ prune() {
25
+ const now = Date.now();
26
+ for (const [key, entry] of this.hits) {
27
+ if (now >= entry.resetAt)
28
+ this.hits.delete(key);
29
+ }
30
+ }
31
+ destroy() {
32
+ clearInterval(this.pruneTimer);
33
+ }
34
+ }
35
+ /** A host or a test on this machine: never rate limited. */
36
+ export function isLoopback(ip) {
37
+ const normalized = ip.trim().toLowerCase();
38
+ return normalized === "127.0.0.1" || normalized === "::1" || normalized === "::ffff:127.0.0.1";
39
+ }
40
+ /** IPv4 addresses arrive IPv6-mapped (::ffff:1.2.3.4) on a dual-stack socket. */
41
+ function normalizeIp(ip) {
42
+ const trimmed = ip.trim().toLowerCase();
43
+ return trimmed.startsWith("::ffff:") ? trimmed.slice(7) : trimmed;
44
+ }
45
+ /** The reverse proxies in front of this gateway, from a list of their addresses. */
46
+ export function trustedProxySet(addresses) {
47
+ return new Set(addresses.map(normalizeIp).filter(Boolean));
48
+ }
49
+ /**
50
+ * Who is connecting. X-Forwarded-For is believed only when the direct peer is
51
+ * one of `trustedProxies`: from anyone else the header is the caller's own
52
+ * claim, and believing it would let them pick their rate-limit key.
53
+ */
54
+ export function clientIp(request, trustedProxies) {
55
+ const peer = request.socket.remoteAddress ?? "unknown";
56
+ if (trustedProxies.has(normalizeIp(peer))) {
57
+ const forwarded = request.headers["x-forwarded-for"];
58
+ if (typeof forwarded === "string") {
59
+ const first = forwarded.split(",")[0]?.trim();
60
+ if (first)
61
+ return first;
62
+ }
63
+ }
64
+ return peer;
65
+ }
66
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../../../gateway/src/rate-limit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ import { type IncomingMessage } from "node:http";
2
+ import type { Duplex } from "node:stream";
3
+ import { type PeerRole } from "@linkshell/wire";
4
+ import type { VerifyToken } from "./accounts.js";
5
+ import { GatewayStore } from "./store.js";
6
+ export interface GatewayOptions {
7
+ port: number;
8
+ host?: string;
9
+ databasePath: string;
10
+ /** Validates account tokens; without it, accounts are off and only pairing works. */
11
+ verifyToken?: VerifyToken;
12
+ log?: (message: string) => void;
13
+ /** Called with every frame the gateway routes, for tests that check it only sees ciphertext. */
14
+ onRouted?: (frame: string) => void;
15
+ pairingTtlMs?: number;
16
+ heartbeatMs?: number;
17
+ /**
18
+ * Final say on a connecting peer, after its key (and token, if any) checked
19
+ * out: return a message to refuse it. The official gateway uses this to
20
+ * require a subscription for computers.
21
+ */
22
+ admit?: (peer: {
23
+ role: PeerRole;
24
+ userId?: string;
25
+ }) => Promise<string | undefined>;
26
+ /**
27
+ * How much may wait to be written to one peer before the gateway stops
28
+ * reading from whoever is sending to it (default 4 MB), and how long a peer
29
+ * may stay that far behind before it is dropped (default 60 s).
30
+ */
31
+ maxBufferedBytes?: number;
32
+ stalledPeerMs?: number;
33
+ }
34
+ /**
35
+ * Gateway v2: authenticates peers by key, routes their end-to-end encrypted
36
+ * frames to peers they may reach (paired, or same account), and brokers
37
+ * pairing. It has no way to read what it routes.
38
+ */
39
+ export declare class Gateway {
40
+ private readonly options;
41
+ readonly store: GatewayStore;
42
+ private server?;
43
+ private readonly peers;
44
+ private readonly offers;
45
+ private readonly decisions;
46
+ private readonly claimTimes;
47
+ private heartbeat?;
48
+ /** Sockets that answered the last ping. */
49
+ private readonly alive;
50
+ private stopped;
51
+ private readonly log;
52
+ constructor(options: GatewayOptions);
53
+ private readonly wss;
54
+ /** Runs on its own port. */
55
+ start(): Promise<number>;
56
+ /**
57
+ * Runs inside another HTTP server, which passes it upgrades for
58
+ * `RELAY_PATH` through `handleUpgrade`.
59
+ */
60
+ attach(): void;
61
+ handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void;
62
+ get connected(): number;
63
+ /** Bytes accepted from senders and not yet written to their receivers. */
64
+ get bufferedBytes(): number;
65
+ /** Drops connections that didn't answer the previous ping, pings the rest. */
66
+ private sweep;
67
+ stop(): Promise<void>;
68
+ private accept;
69
+ private authenticate;
70
+ private route;
71
+ /**
72
+ * Backpressure. What a peer can't take yet would otherwise pile up here, in
73
+ * memory every user of this gateway shares: stop reading the sender instead,
74
+ * so the data waits on its side until the receiver catches up.
75
+ */
76
+ private holdWhileBehind;
77
+ private event;
78
+ /** Tells everyone who can reach `record` that it came or went. */
79
+ private announce;
80
+ private machineEntry;
81
+ private deviceEntry;
82
+ private handle;
83
+ private claim;
84
+ }