@kdonev/termscape 0.1.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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +288 -0
  3. package/dist/agents/env.d.ts +36 -0
  4. package/dist/agents/env.d.ts.map +1 -0
  5. package/dist/agents/env.js +61 -0
  6. package/dist/agents/env.js.map +1 -0
  7. package/dist/agents/profiles.d.ts +45 -0
  8. package/dist/agents/profiles.d.ts.map +1 -0
  9. package/dist/agents/profiles.js +123 -0
  10. package/dist/agents/profiles.js.map +1 -0
  11. package/dist/agents/resolve.d.ts +16 -0
  12. package/dist/agents/resolve.d.ts.map +1 -0
  13. package/dist/agents/resolve.js +71 -0
  14. package/dist/agents/resolve.js.map +1 -0
  15. package/dist/agents/router.d.ts +29 -0
  16. package/dist/agents/router.d.ts.map +1 -0
  17. package/dist/agents/router.js +83 -0
  18. package/dist/agents/router.js.map +1 -0
  19. package/dist/agents/tokens.d.ts +21 -0
  20. package/dist/agents/tokens.d.ts.map +1 -0
  21. package/dist/agents/tokens.js +39 -0
  22. package/dist/agents/tokens.js.map +1 -0
  23. package/dist/agents/wiring.d.ts +30 -0
  24. package/dist/agents/wiring.d.ts.map +1 -0
  25. package/dist/agents/wiring.js +110 -0
  26. package/dist/agents/wiring.js.map +1 -0
  27. package/dist/browser.d.ts +13 -0
  28. package/dist/browser.d.ts.map +1 -0
  29. package/dist/browser.js +34 -0
  30. package/dist/browser.js.map +1 -0
  31. package/dist/cli.d.ts +3 -0
  32. package/dist/cli.d.ts.map +1 -0
  33. package/dist/cli.js +200 -0
  34. package/dist/cli.js.map +1 -0
  35. package/dist/db/index.d.ts +14 -0
  36. package/dist/db/index.d.ts.map +1 -0
  37. package/dist/db/index.js +24 -0
  38. package/dist/db/index.js.map +1 -0
  39. package/dist/db/migrations.d.ts +15 -0
  40. package/dist/db/migrations.d.ts.map +1 -0
  41. package/dist/db/migrations.js +176 -0
  42. package/dist/db/migrations.js.map +1 -0
  43. package/dist/db/store.d.ts +71 -0
  44. package/dist/db/store.d.ts.map +1 -0
  45. package/dist/db/store.js +341 -0
  46. package/dist/db/store.js.map +1 -0
  47. package/dist/hub.d.ts +188 -0
  48. package/dist/hub.d.ts.map +1 -0
  49. package/dist/hub.js +636 -0
  50. package/dist/hub.js.map +1 -0
  51. package/dist/hub.tgz +0 -0
  52. package/dist/mcp/server.d.ts +31 -0
  53. package/dist/mcp/server.d.ts.map +1 -0
  54. package/dist/mcp/server.js +66 -0
  55. package/dist/mcp/server.js.map +1 -0
  56. package/dist/paths.d.ts +27 -0
  57. package/dist/paths.d.ts.map +1 -0
  58. package/dist/paths.js +31 -0
  59. package/dist/paths.js.map +1 -0
  60. package/dist/protocol/address.d.ts +15 -0
  61. package/dist/protocol/address.d.ts.map +1 -0
  62. package/dist/protocol/address.js +39 -0
  63. package/dist/protocol/address.js.map +1 -0
  64. package/dist/protocol/domain.d.ts +151 -0
  65. package/dist/protocol/domain.d.ts.map +1 -0
  66. package/dist/protocol/domain.js +104 -0
  67. package/dist/protocol/domain.js.map +1 -0
  68. package/dist/protocol/index.d.ts +7 -0
  69. package/dist/protocol/index.d.ts.map +1 -0
  70. package/dist/protocol/index.js +7 -0
  71. package/dist/protocol/index.js.map +1 -0
  72. package/dist/protocol/inject.d.ts +29 -0
  73. package/dist/protocol/inject.d.ts.map +1 -0
  74. package/dist/protocol/inject.js +40 -0
  75. package/dist/protocol/inject.js.map +1 -0
  76. package/dist/protocol/mcp-tools.d.ts +45 -0
  77. package/dist/protocol/mcp-tools.d.ts.map +1 -0
  78. package/dist/protocol/mcp-tools.js +46 -0
  79. package/dist/protocol/mcp-tools.js.map +1 -0
  80. package/dist/protocol/peer.d.ts +304 -0
  81. package/dist/protocol/peer.d.ts.map +1 -0
  82. package/dist/protocol/peer.js +183 -0
  83. package/dist/protocol/peer.js.map +1 -0
  84. package/dist/protocol/ws.d.ts +416 -0
  85. package/dist/protocol/ws.d.ts.map +1 -0
  86. package/dist/protocol/ws.js +119 -0
  87. package/dist/protocol/ws.js.map +1 -0
  88. package/dist/remote/deployer.d.ts +78 -0
  89. package/dist/remote/deployer.d.ts.map +1 -0
  90. package/dist/remote/deployer.js +216 -0
  91. package/dist/remote/deployer.js.map +1 -0
  92. package/dist/remote/enroll.d.ts +14 -0
  93. package/dist/remote/enroll.d.ts.map +1 -0
  94. package/dist/remote/enroll.js +244 -0
  95. package/dist/remote/enroll.js.map +1 -0
  96. package/dist/remote/join-script.d.ts +29 -0
  97. package/dist/remote/join-script.d.ts.map +1 -0
  98. package/dist/remote/join-script.js +709 -0
  99. package/dist/remote/join-script.js.map +1 -0
  100. package/dist/remote/join.d.ts +19 -0
  101. package/dist/remote/join.d.ts.map +1 -0
  102. package/dist/remote/join.js +195 -0
  103. package/dist/remote/join.js.map +1 -0
  104. package/dist/remote/lan.d.ts +45 -0
  105. package/dist/remote/lan.d.ts.map +1 -0
  106. package/dist/remote/lan.js +105 -0
  107. package/dist/remote/lan.js.map +1 -0
  108. package/dist/remote/peer-serve.d.ts +27 -0
  109. package/dist/remote/peer-serve.d.ts.map +1 -0
  110. package/dist/remote/peer-serve.js +274 -0
  111. package/dist/remote/peer-serve.js.map +1 -0
  112. package/dist/remote/peer.d.ts +62 -0
  113. package/dist/remote/peer.d.ts.map +1 -0
  114. package/dist/remote/peer.js +250 -0
  115. package/dist/remote/peer.js.map +1 -0
  116. package/dist/remote/registry.d.ts +104 -0
  117. package/dist/remote/registry.d.ts.map +1 -0
  118. package/dist/remote/registry.js +359 -0
  119. package/dist/remote/registry.js.map +1 -0
  120. package/dist/remote/tarball.d.ts +9 -0
  121. package/dist/remote/tarball.d.ts.map +1 -0
  122. package/dist/remote/tarball.js +22 -0
  123. package/dist/remote/tarball.js.map +1 -0
  124. package/dist/server.d.ts +54 -0
  125. package/dist/server.d.ts.map +1 -0
  126. package/dist/server.js +438 -0
  127. package/dist/server.js.map +1 -0
  128. package/dist/session/manager.d.ts +104 -0
  129. package/dist/session/manager.d.ts.map +1 -0
  130. package/dist/session/manager.js +421 -0
  131. package/dist/session/manager.js.map +1 -0
  132. package/dist/session/pty.d.ts +114 -0
  133. package/dist/session/pty.d.ts.map +1 -0
  134. package/dist/session/pty.js +300 -0
  135. package/dist/session/pty.js.map +1 -0
  136. package/package.json +49 -0
  137. package/web/assets/index-B2JtCtHa.css +1 -0
  138. package/web/assets/index-BOqZOoep.js +107 -0
  139. package/web/index.html +13 -0
@@ -0,0 +1,216 @@
1
+ import { createServer } from 'node:net';
2
+ import { readFileSync } from 'node:fs';
3
+ import { basename } from 'node:path';
4
+ import { Client as SshClient } from 'ssh2';
5
+ const DEFAULT_REMOTE_DIR = '~/.termscape';
6
+ function connectConfig(c) {
7
+ const cfg = {
8
+ host: c.sshHost,
9
+ port: c.sshPort,
10
+ username: c.sshUser,
11
+ readyTimeout: 20_000,
12
+ };
13
+ if (c.privateKeyPath) {
14
+ cfg.privateKey = readFileSync(c.privateKeyPath);
15
+ if (c.passphrase)
16
+ cfg.passphrase = c.passphrase;
17
+ }
18
+ else {
19
+ // Fall back to the user's running SSH agent, which is how most people
20
+ // already authenticate; we never prompt for or store a password.
21
+ cfg.agent = process.env.SSH_AUTH_SOCK ?? (process.platform === 'win32' ? 'pageant' : undefined);
22
+ }
23
+ return cfg;
24
+ }
25
+ export function sshConnect(c) {
26
+ return new Promise((resolve, reject) => {
27
+ const conn = new SshClient();
28
+ conn.on('ready', () => resolve(conn));
29
+ conn.on('error', (err) => reject(new Error(`ssh ${c.sshUser}@${c.sshHost}: ${err.message}`)));
30
+ conn.connect(connectConfig(c));
31
+ });
32
+ }
33
+ export function exec(conn, command) {
34
+ return new Promise((resolve, reject) => {
35
+ conn.exec(command, (err, stream) => {
36
+ if (err)
37
+ return reject(err);
38
+ let stdout = '';
39
+ let stderr = '';
40
+ stream.on('data', (d) => (stdout += d.toString()));
41
+ stream.stderr.on('data', (d) => (stderr += d.toString()));
42
+ stream.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
43
+ });
44
+ });
45
+ }
46
+ /** What is already on the remote machine, before we change anything. */
47
+ export async function probe(conn, remoteDir = DEFAULT_REMOTE_DIR) {
48
+ const node = await exec(conn, 'node --version 2>/dev/null || true');
49
+ const nodeVersion = node.stdout.trim() || null;
50
+ const nodeMajor = nodeVersion ? Number(nodeVersion.replace(/^v/, '').split('.')[0]) : 0;
51
+ const installed = await exec(conn, `cat ${remoteDir}/hub/package.json 2>/dev/null | grep '"version"' || true`);
52
+ const m = installed.stdout.match(/"version"\s*:\s*"([^"]+)"/);
53
+ return { nodeVersion, nodeMajor, installedVersion: m?.[1] ?? null };
54
+ }
55
+ async function upload(conn, localPath, remotePath) {
56
+ return new Promise((resolve, reject) => {
57
+ conn.sftp((err, sftp) => {
58
+ if (err)
59
+ return reject(err);
60
+ sftp.fastPut(localPath, remotePath, (e) => (e ? reject(e) : resolve()));
61
+ });
62
+ });
63
+ }
64
+ /**
65
+ * Install or upgrade the hub on the remote machine. Native modules are rebuilt
66
+ * there, because node-pty and better-sqlite3 prebuilds are per platform and
67
+ * per architecture and the remote is frequently neither.
68
+ */
69
+ export async function provision(conn, opts) {
70
+ const remoteDir = opts.remoteDir ?? DEFAULT_REMOTE_DIR;
71
+ const log = opts.log ?? (() => { });
72
+ if (!opts.packagePath) {
73
+ throw new Error('remote hub is missing or out of date and no package tarball was supplied; ' +
74
+ 'run `npm pack -w @termscape/hub` and pass its path');
75
+ }
76
+ const tarball = basename(opts.packagePath);
77
+ log(`uploading ${tarball}`);
78
+ await exec(conn, `mkdir -p ${remoteDir}`);
79
+ await upload(conn, opts.packagePath, `${remoteDir}/${tarball}`);
80
+ log('installing on remote');
81
+ // The dependency tree is the slowest thing to rebuild over there and the
82
+ // tarball never carries one, so it is moved aside rather than deleted along
83
+ // with the install it lives in.
84
+ await exec(conn, `cd ${remoteDir} && rm -rf node_modules.kept` +
85
+ ` && if [ -d hub/node_modules ]; then mv hub/node_modules node_modules.kept; fi` +
86
+ ` && rm -rf hub && mkdir -p hub && tar xzf ${tarball} -C hub --strip-components=1` +
87
+ ` && if [ -d node_modules.kept ]; then mv node_modules.kept hub/node_modules; fi`);
88
+ // What the tree already there has to match to be worth keeping: the
89
+ // fingerprint the tarball shipped, plus the two things it cannot know - the
90
+ // Node ABI those modules were built against, and the platform.
91
+ const stamp = `"$(cat deps.fingerprint 2>/dev/null || echo none)-` +
92
+ `$(node -p 'process.versions.node.split(".")[0] + "-" + process.platform + "-" + process.arch')"`;
93
+ // The stamp claims the tree still works; the require confirms it does.
94
+ // Trusting the stamp alone would trade a slow deploy for a remote hub that
95
+ // cannot load its own modules.
96
+ const reusable = await exec(conn, `cd ${remoteDir}/hub && [ -d node_modules ]` +
97
+ ` && [ "$(cat ../deps.stamp 2>/dev/null)" = ${stamp} ]` +
98
+ ` && node -e 'require("node-pty"); require("better-sqlite3")'`);
99
+ if (reusable.code === 0) {
100
+ log('dependencies unchanged; keeping the modules already there');
101
+ return;
102
+ }
103
+ // A half-finished tree must not inherit the last deploy's stamp.
104
+ await exec(conn, `rm -f ${remoteDir}/deps.stamp`);
105
+ // Prebuilt binaries first: node-pty and better-sqlite3 publish them for the
106
+ // mainstream platforms, and downloading one beats compiling it every time.
107
+ let install = await exec(conn, `cd ${remoteDir}/hub && npm install --omit=dev --no-audit --no-fund 2>&1 | tail -20`);
108
+ if (install.code !== 0) {
109
+ log('no prebuilt binaries for this platform; compiling instead');
110
+ install = await exec(conn, `cd ${remoteDir}/hub && npm install --omit=dev --no-audit --no-fund --build-from-source 2>&1 | tail -20`);
111
+ }
112
+ if (install.code !== 0) {
113
+ throw new Error(`remote install failed (exit ${install.code}). This host has no prebuilt ` +
114
+ `binaries and no C++ toolchain for node-pty and better-sqlite3.\n` +
115
+ `${install.stdout}\n${install.stderr}`);
116
+ }
117
+ await exec(conn, `cd ${remoteDir}/hub && printf '%s' ${stamp} > ../deps.stamp`);
118
+ log('installed');
119
+ }
120
+ /**
121
+ * Start the remote hub detached, so it outlives this SSH session — the whole
122
+ * reason for running a daemon instead of an ssh subprocess.
123
+ */
124
+ export async function startRemoteHub(conn, opts) {
125
+ const remoteDir = opts.remoteDir ?? DEFAULT_REMOTE_DIR;
126
+ const logFile = `${remoteDir}/hub.log`;
127
+ // Reuse an already-running daemon rather than starting a second one.
128
+ const existing = await exec(conn, `cat ${remoteDir}/hub.port 2>/dev/null || true`);
129
+ const existingPort = Number(existing.stdout.trim());
130
+ if (existingPort > 0) {
131
+ const alive = await exec(conn, `curl -s -m 2 http://127.0.0.1:${existingPort}/health || true`);
132
+ const m = alive.stdout.match(/"hubVersion"\s*:\s*"([^"]+)"/);
133
+ if (m)
134
+ return { port: existingPort, version: m[1] };
135
+ }
136
+ await exec(conn, `cd ${remoteDir}/hub && TERMSCAPE_HOME=${remoteDir} nohup node dist/cli.js --headless --port 0 --token '${opts.token}' > ${logFile} 2>&1 &`);
137
+ // The CLI prints TERMSCAPE_PORT=<n> precisely so this can be parsed.
138
+ for (let i = 0; i < 40; i++) {
139
+ await new Promise((r) => setTimeout(r, 250));
140
+ const out = await exec(conn, `grep -m1 TERMSCAPE_PORT= ${logFile} 2>/dev/null || true`);
141
+ const m = out.stdout.match(/TERMSCAPE_PORT=(\d+)/);
142
+ if (m) {
143
+ const port = Number(m[1]);
144
+ await exec(conn, `echo ${port} > ${remoteDir}/hub.port`);
145
+ const v = await exec(conn, `curl -s -m 3 http://127.0.0.1:${port}/health || true`);
146
+ const vm = v.stdout.match(/"hubVersion"\s*:\s*"([^"]+)"/);
147
+ return { port, version: vm?.[1] ?? 'unknown' };
148
+ }
149
+ }
150
+ const tail = await exec(conn, `tail -30 ${logFile} 2>/dev/null || true`);
151
+ throw new Error(`remote hub did not report a port.\n${tail.stdout}`);
152
+ }
153
+ /**
154
+ * Local TCP listener that pipes every connection through the SSH channel to
155
+ * the remote hub's loopback port. Gives us a plain ws:// URL to hand the peer
156
+ * client, which keeps SSH entirely out of the peer protocol.
157
+ */
158
+ export function openTunnel(conn, remotePort) {
159
+ return new Promise((resolve, reject) => {
160
+ const server = createServer((socket) => {
161
+ conn.forwardOut('127.0.0.1', 0, '127.0.0.1', remotePort, (err, stream) => {
162
+ if (err) {
163
+ socket.destroy();
164
+ return;
165
+ }
166
+ socket.pipe(stream).pipe(socket);
167
+ stream.on('error', () => socket.destroy());
168
+ socket.on('error', () => stream.destroy());
169
+ });
170
+ });
171
+ server.on('error', reject);
172
+ // Loopback only: the tunnel entrance must not be reachable from the network.
173
+ server.listen(0, '127.0.0.1', () => {
174
+ const addr = server.address();
175
+ if (!addr || typeof addr === 'string')
176
+ return reject(new Error('tunnel bind failed'));
177
+ resolve({ localPort: addr.port, server });
178
+ });
179
+ });
180
+ }
181
+ /** Probe, provision if needed, start, and tunnel. */
182
+ export async function deploy(opts) {
183
+ const log = opts.log ?? (() => { });
184
+ const conn = await sshConnect(opts);
185
+ try {
186
+ const p = await probe(conn, opts.remoteDir);
187
+ if (!p.nodeVersion) {
188
+ throw new Error('no node on the remote host; install Node 22 or newer there first');
189
+ }
190
+ if (p.nodeMajor < 22) {
191
+ throw new Error(`remote node is ${p.nodeVersion}; this hub needs Node 22 or newer`);
192
+ }
193
+ log(`remote node ${p.nodeVersion}, hub ${p.installedVersion ?? 'not installed'}`);
194
+ if (p.installedVersion !== opts.expectedVersion) {
195
+ await provision(conn, opts);
196
+ }
197
+ const { port, version } = await startRemoteHub(conn, opts);
198
+ log(`remote hub ${version} on 127.0.0.1:${port}`);
199
+ const { localPort, server } = await openTunnel(conn, port);
200
+ log(`tunnel ready on 127.0.0.1:${localPort}`);
201
+ return {
202
+ localUrl: `ws://127.0.0.1:${localPort}/peer`,
203
+ remotePort: port,
204
+ remoteHubVersion: version,
205
+ dispose: async () => {
206
+ await new Promise((res) => server.close(() => res()));
207
+ conn.end();
208
+ },
209
+ };
210
+ }
211
+ catch (err) {
212
+ conn.end();
213
+ throw err;
214
+ }
215
+ }
216
+ //# sourceMappingURL=deployer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deployer.js","sourceRoot":"","sources":["../../src/remote/deployer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { Hub } from '../hub.js';
3
+ export interface EnrollOptions {
4
+ hub: Hub;
5
+ /** Origin another machine can reach us on, or null when we are loopback-bound. */
6
+ enrollOrigin: () => string | null;
7
+ }
8
+ export interface Enrollment {
9
+ /** Mint a single-use token for one download. */
10
+ mint(): string;
11
+ pendingCount(): number;
12
+ }
13
+ export declare function registerEnrollment(app: FastifyInstance, opts: EnrollOptions): Enrollment;
14
+ //# sourceMappingURL=enroll.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enroll.d.ts","sourceRoot":"","sources":["../../src/remote/enroll.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAQ/C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AA0BrC,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,GAAG,CAAC;IACT,kFAAkF;IAClF,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,UAAU;IACzB,gDAAgD;IAChD,IAAI,IAAI,MAAM,CAAC;IACf,YAAY,IAAI,MAAM,CAAC;CACxB;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,aAAa,GAClB,UAAU,CAoKZ"}
@@ -0,0 +1,244 @@
1
+ import { randomBytes, randomUUID } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { PeerRequest, PEER_SCHEMA_VERSION, } from '../protocol/index.js';
4
+ import { HUB_VERSION } from '../hub.js';
5
+ import { joinScriptPosix, joinScriptPowerShell } from './join-script.js';
6
+ import { hubTarballPath } from './tarball.js';
7
+ /**
8
+ * The pull half of attaching a machine.
9
+ *
10
+ * Instead of this hub reaching out over SSH, the other machine opens this
11
+ * hub's page, downloads an installer, runs it, and dials back. That inverts
12
+ * who needs credentials — nobody types an SSH key here — and it puts install
13
+ * failures in the terminal of the person who can actually fix them.
14
+ *
15
+ * Enrollment tokens are single-use and short-lived, and live in memory only.
16
+ * A hub restart invalidates outstanding downloads, which is the right trade
17
+ * for a credential that is supposed to be spent within minutes of minting.
18
+ */
19
+ const ENROLL_TTL_MS = 15 * 60 * 1000;
20
+ /**
21
+ * Minting is unauthenticated, so it has to be bounded. Nobody has 64 machines
22
+ * mid-join at once; anything past that is someone hammering the page, and the
23
+ * oldest unspent token is the safest thing to drop.
24
+ */
25
+ const MAX_PENDING = 64;
26
+ export function registerEnrollment(app, opts) {
27
+ const { hub, enrollOrigin } = opts;
28
+ /** token -> expiry. Single-use: matching deletes. */
29
+ const pending = new Map();
30
+ function sweep() {
31
+ const now = Date.now();
32
+ for (const [token, expires] of pending) {
33
+ if (expires <= now)
34
+ pending.delete(token);
35
+ }
36
+ }
37
+ function mint() {
38
+ sweep();
39
+ while (pending.size >= MAX_PENDING) {
40
+ // Insertion-ordered, so the first key is the oldest still-unspent token.
41
+ pending.delete(pending.keys().next().value);
42
+ }
43
+ const token = randomBytes(24).toString('base64url');
44
+ pending.set(token, Date.now() + ENROLL_TTL_MS);
45
+ return token;
46
+ }
47
+ function consume(token) {
48
+ sweep();
49
+ if (!pending.has(token))
50
+ return false;
51
+ pending.delete(token);
52
+ return true;
53
+ }
54
+ /* ------------------------------------------------------------- the page */
55
+ /**
56
+ * The one route served without the client token. It is reachable only when
57
+ * the hub was deliberately bound off loopback, and it exposes nothing but a
58
+ * command to copy.
59
+ */
60
+ app.get('/join', async (_req, reply) => {
61
+ const origin = enrollOrigin();
62
+ if (!origin) {
63
+ return reply
64
+ .code(404)
65
+ .type('text/html; charset=utf-8')
66
+ .send(notReachablePage());
67
+ }
68
+ return reply
69
+ .type('text/html; charset=utf-8')
70
+ .header('cache-control', 'no-store')
71
+ .send(joinPage(origin));
72
+ });
73
+ app.get('/join.sh', async (_req, reply) => {
74
+ const origin = enrollOrigin();
75
+ if (!origin)
76
+ return reply.code(404).send('# this hub is not reachable from other machines\n');
77
+ return reply
78
+ .type('text/x-shellscript; charset=utf-8')
79
+ .header('cache-control', 'no-store')
80
+ .send(joinScriptPosix(origin, mint()));
81
+ });
82
+ app.get('/join.ps1', async (_req, reply) => {
83
+ const origin = enrollOrigin();
84
+ if (!origin)
85
+ return reply.code(404).send('# this hub is not reachable from other machines\n');
86
+ return reply
87
+ .type('text/plain; charset=utf-8')
88
+ .header('cache-control', 'no-store')
89
+ .send(joinScriptPowerShell(origin, mint()));
90
+ });
91
+ app.get('/hub.tgz', async (_req, reply) => {
92
+ const tarball = hubTarballPath();
93
+ if (!tarball) {
94
+ return reply
95
+ .code(503)
96
+ .send('hub package not built; run `npm run build` on the hub machine\n');
97
+ }
98
+ return reply.type('application/gzip').send(createReadStream(tarball));
99
+ });
100
+ /* --------------------------------------------------------- inbound peers */
101
+ /**
102
+ * A host dialling in. It speaks first, exactly as we would if we had dialled
103
+ * it; the only difference is that its token is an enrollment or host token
104
+ * rather than one we minted for a deploy.
105
+ */
106
+ app.get('/peer-in', { websocket: true }, (socket) => {
107
+ let settled = false;
108
+ const reply = (msg) => {
109
+ if (socket.readyState === socket.OPEN)
110
+ socket.send(JSON.stringify(msg));
111
+ };
112
+ const refuse = (message) => {
113
+ reply({ t: 'err', id: 'hello', message });
114
+ socket.close();
115
+ };
116
+ const onHello = (raw) => {
117
+ // Everything after the handshake belongs to the PeerConnection that
118
+ // adopts this socket, not to us.
119
+ if (settled)
120
+ return;
121
+ let parsed;
122
+ try {
123
+ parsed = PeerRequest.safeParse(JSON.parse(raw.toString('utf8')));
124
+ }
125
+ catch {
126
+ return refuse('unparseable hello');
127
+ }
128
+ if (!parsed.success || parsed.data.t !== 'hello') {
129
+ return refuse('expected hello');
130
+ }
131
+ const hello = parsed.data;
132
+ if (hello.schemaVersion !== PEER_SCHEMA_VERSION) {
133
+ return refuse(`peer schema mismatch: you speak v${hello.schemaVersion}, this hub speaks ` +
134
+ `v${PEER_SCHEMA_VERSION}. Re-run the join command to get a matching hub.`);
135
+ }
136
+ let host = null;
137
+ let issuedToken;
138
+ const known = hub.store.hostByToken(hello.token);
139
+ if (known) {
140
+ // A host we have seen before, coming back after a restart or a drop.
141
+ host = { ...known, hubVersion: hello.hubVersion };
142
+ hub.store.upsertHost(host);
143
+ }
144
+ else if (consume(hello.token)) {
145
+ if (!hello.enroll)
146
+ return refuse('enrollment token used without host details');
147
+ issuedToken = randomBytes(24).toString('base64url');
148
+ host = {
149
+ id: randomUUID(),
150
+ label: hello.enroll.label,
151
+ kind: 'enrolled',
152
+ sshHost: null,
153
+ sshUser: null,
154
+ sshPort: 22,
155
+ platform: `${hello.enroll.platform}-${hello.enroll.arch}`,
156
+ hubVersion: hello.hubVersion,
157
+ state: 'connecting',
158
+ lastSeenAt: null,
159
+ error: null,
160
+ };
161
+ hub.store.upsertHost({ ...host, hostToken: issuedToken });
162
+ hub.emit('host', host);
163
+ }
164
+ else {
165
+ return refuse('unknown or expired enrollment token; open the join page again');
166
+ }
167
+ settled = true;
168
+ reply({
169
+ t: 'welcome',
170
+ hubVersion: HUB_VERSION,
171
+ schemaVersion: PEER_SCHEMA_VERSION,
172
+ ...(issuedToken ? { hostToken: issuedToken } : {}),
173
+ });
174
+ hub.peers.addInbound(host, socket, hello.hubVersion);
175
+ };
176
+ socket.on('message', onHello);
177
+ });
178
+ return { mint, pendingCount: () => (sweep(), pending.size) };
179
+ }
180
+ /* ------------------------------------------------------------------ pages */
181
+ const PAGE_CSS = `
182
+ :root { color-scheme: dark; }
183
+ body {
184
+ margin: 0; padding: 48px 24px;
185
+ background: #0a0c10; color: #d5dae2;
186
+ font: 14px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
187
+ }
188
+ main { max-width: 660px; margin: 0 auto; }
189
+ h1 { font-size: 22px; font-weight: 600; margin: 0 0 4px; }
190
+ p.lede { color: #7c8596; margin: 0 0 32px; }
191
+ h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .08em;
192
+ color: #7c8596; font-weight: 600; margin: 28px 0 8px; }
193
+ pre {
194
+ background: #12161e; border: 1px solid #262d3b; border-radius: 6px;
195
+ padding: 12px 14px; overflow-x: auto; margin: 0;
196
+ font: 12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
197
+ color: #d5dae2;
198
+ }
199
+ .note { color: #7c8596; font-size: 12.5px; margin-top: 10px; }
200
+ .warn { color: #d8b271; }
201
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
202
+ `;
203
+ function joinPage(origin) {
204
+ return `<!doctype html>
205
+ <meta charset="utf-8">
206
+ <meta name="viewport" content="width=device-width, initial-scale=1">
207
+ <title>Join this canvas</title>
208
+ <style>${PAGE_CSS}</style>
209
+ <main>
210
+ <h1>Add this machine to the canvas</h1>
211
+ <p class="lede">Run one of these here. It installs the hub under
212
+ <code>~/.termscape</code> and connects back to <code>${escapeHtml(origin)}</code>.</p>
213
+
214
+ <h2>macOS &middot; Linux</h2>
215
+ <pre>curl -fsSL ${escapeHtml(origin)}/join.sh | sh</pre>
216
+
217
+ <h2>Windows (PowerShell)</h2>
218
+ <pre>irm ${escapeHtml(origin)}/join.ps1 | iex</pre>
219
+
220
+ <p class="note">Needs Node 22 or newer. The installer says exactly what is
221
+ missing before it changes anything.</p>
222
+ <p class="note warn">Each download carries its own single-use key, good for
223
+ 15 minutes. Run the command soon after copying it.</p>
224
+ </main>
225
+ `;
226
+ }
227
+ function notReachablePage() {
228
+ return `<!doctype html>
229
+ <meta charset="utf-8">
230
+ <title>Not reachable</title>
231
+ <style>${PAGE_CSS}</style>
232
+ <main>
233
+ <h1>This hub is bound to loopback</h1>
234
+ <p class="lede">Nothing outside this machine can reach it, so there is
235
+ nothing to join yet.</p>
236
+ <p class="note">Restart the hub with <code>--listen lan</code> to make it
237
+ reachable on your local network, then reload this page.</p>
238
+ </main>
239
+ `;
240
+ }
241
+ function escapeHtml(s) {
242
+ return s.replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`);
243
+ }
244
+ //# sourceMappingURL=enroll.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enroll.js","sourceRoot":"","sources":["../../src/remote/enroll.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The installers a joining machine downloads and runs.
3
+ *
4
+ * They are generated rather than shipped as files so the hub's own origin and
5
+ * a freshly minted single-use token are baked in — the person running this has
6
+ * nothing to configure and nothing to paste twice.
7
+ *
8
+ * Both scripts follow the same order, and it matters: work out what is missing
9
+ * first, and say precisely what is happening *before* changing anything. The
10
+ * SSH deploy path could only report a failure as a wall of build output in a
11
+ * tooltip; here the message lands in the terminal of the person standing at
12
+ * the keyboard.
13
+ *
14
+ * They are also deliberately chatty. Two steps take a minute or more — pulling
15
+ * ~30MB of Node, and an `npm install` that may compile native modules — and a
16
+ * terminal that prints nothing for that long is indistinguishable from one
17
+ * that has hung. Every long step therefore says what it is about to do, ticks
18
+ * while it runs, and reports how long it took.
19
+ *
20
+ * When Node is missing or too old, a private copy is fetched into
21
+ * `~/.termscape/node` rather than installed system-wide. That is deliberate:
22
+ * it needs no administrator rights, no package manager, and no PATH surgery
23
+ * that a fresh shell would have to pick up; and uninstalling is deleting one
24
+ * directory. The download is checksum-verified against nodejs.org's own
25
+ * SHASUMS256.txt, because this is a binary we are about to execute.
26
+ */
27
+ export declare function joinScriptPosix(origin: string, token: string): string;
28
+ export declare function joinScriptPowerShell(origin: string, token: string): string;
29
+ //# sourceMappingURL=join-script.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"join-script.d.ts","sourceRoot":"","sources":["../../src/remote/join-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAUH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CA8SrE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAoX1E"}