pinomad 0.9.1 → 0.11.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.
@@ -1,9 +1,9 @@
1
- import {
2
- packagedVersion
3
- } from "./chunk-fqgvf8v0.js";
4
1
  import {
5
2
  hostAddress
6
3
  } from "./chunk-ngf1zatm.js";
4
+ import {
5
+ packagedVersion
6
+ } from "./chunk-sd5fkxjj.js";
7
7
  import"./chunk-df32avw6.js";
8
8
 
9
9
  // apps/host/src/cli/web-link.ts
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ readServerConfig
4
+ } from "./chunk-etf7ef97.js";
5
+ import {
6
+ argValue,
7
+ reachabilityLine,
8
+ serviceManager,
9
+ versionLine
10
+ } from "./chunk-vvj9b1gc.js";
11
+ import"./chunk-jt34g8st.js";
12
+ import"./chunk-kzbzacz9.js";
13
+ import"./chunk-xqycs43g.js";
14
+ import"./chunk-pqe7q1s7.js";
15
+ import {
16
+ DEFAULT_DATA_DIR
17
+ } from "./chunk-ngf1zatm.js";
18
+ import {
19
+ commandHint
20
+ } from "./chunk-sd5fkxjj.js";
21
+ import"./chunk-df32avw6.js";
22
+
23
+ // apps/host/src/cli/status.ts
24
+ import { resolve } from "node:path";
25
+ import { parseArgs } from "node:util";
26
+ async function statusReport(args, deps) {
27
+ const dataDir = resolve(args.dataDir);
28
+ const manager = deps?.manager ?? serviceManager();
29
+ const server = await readServerConfig(dataDir);
30
+ const service = await manager.state();
31
+ const spec = service.installed ? await manager.read() : undefined;
32
+ const own = spec !== undefined && resolve(spec.dataDir) === dataDir;
33
+ const lanPort = own ? argValue(spec.hostArgs, "--remote-port") : undefined;
34
+ const port = own ? argValue(spec.hostArgs, "--port") : undefined;
35
+ return {
36
+ ...server === undefined ? {} : { server },
37
+ ...lanPort === undefined ? {} : { lanPort },
38
+ service,
39
+ ...port === undefined ? {} : { port },
40
+ ...spec === undefined || own ? {} : { otherDataDir: spec.dataDir }
41
+ };
42
+ }
43
+ async function main(argv) {
44
+ const { values } = parseArgs({ args: [...argv], options: { "data-dir": { type: "string" } } });
45
+ const dataDir = resolve(values["data-dir"] ?? DEFAULT_DATA_DIR);
46
+ try {
47
+ const report = await statusReport({ dataDir });
48
+ const { server } = report;
49
+ console.log(server === undefined ? `server: not joined (\`${commandHint("up")}\` to join)` : `server: ${server.username === undefined ? "joined" : server.username} at ${server.origin}`);
50
+ console.log(`LAN and tailnet: ${report.lanPort === undefined ? "off" : `port ${report.lanPort}`}`);
51
+ console.log(`service: ${report.service.installed ? report.service.detail : "not installed"}`);
52
+ if (report.otherDataDir !== undefined)
53
+ console.log(`note: the installed service runs ${report.otherDataDir}, not ${dataDir}`);
54
+ console.log(await versionLine());
55
+ console.log(await reachabilityLine([
56
+ "--data-dir",
57
+ dataDir,
58
+ ...report.port === undefined ? [] : ["--url", `ws://127.0.0.1:${report.port}`]
59
+ ]));
60
+ } catch (error) {
61
+ console.error(error instanceof Error ? error.message : String(error));
62
+ process.exitCode = 1;
63
+ }
64
+ }
65
+ if (false)
66
+ ;
67
+ export {
68
+ statusReport,
69
+ main
70
+ };
@@ -0,0 +1,55 @@
1
+ // apps/host/src/server-config.ts
2
+ import { randomBytes } from "node:crypto";
3
+ import { readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ var SERVER_CONFIG_FILE = "server.json";
6
+ var normalizeOrigin = (origin) => origin.replace(/\/+$/, "");
7
+ async function readServerConfig(dataDir) {
8
+ let text;
9
+ try {
10
+ text = await readFile(join(dataDir, SERVER_CONFIG_FILE), "utf8");
11
+ } catch (error) {
12
+ if (error.code === "ENOENT")
13
+ return;
14
+ throw error;
15
+ }
16
+ let value;
17
+ try {
18
+ value = JSON.parse(text);
19
+ } catch {
20
+ throw new Error(`${join(dataDir, SERVER_CONFIG_FILE)} is not valid JSON — delete it or run \`pinomad leave\``);
21
+ }
22
+ const origin = value?.origin;
23
+ if (typeof origin !== "string" || origin.trim() === "") {
24
+ throw new Error(`${join(dataDir, SERVER_CONFIG_FILE)} is missing a string "origin"`);
25
+ }
26
+ const { binding, username } = value;
27
+ return {
28
+ origin,
29
+ ...typeof binding === "string" && binding !== "" ? { binding } : {},
30
+ ...typeof username === "string" && username !== "" ? { username } : {}
31
+ };
32
+ }
33
+ async function writeServerConfig(dataDir, config) {
34
+ const path = join(dataDir, SERVER_CONFIG_FILE);
35
+ const tmp = `${path}.tmp-${process.pid}`;
36
+ await writeFile(tmp, `${JSON.stringify({
37
+ origin: normalizeOrigin(config.origin),
38
+ binding: randomBytes(16).toString("base64url"),
39
+ ...config.username === undefined ? {} : { username: config.username }
40
+ })}
41
+ `, { mode: 384 });
42
+ await rename(tmp, path);
43
+ }
44
+ var removeServerConfig = (dataDir) => rm(join(dataDir, SERVER_CONFIG_FILE), { force: true });
45
+ async function readServerBinding(dataDir) {
46
+ return (await readServerConfig(dataDir))?.binding;
47
+ }
48
+ function resolveRelayOrigin(flag, config) {
49
+ if (flag !== undefined && config !== undefined && normalizeOrigin(flag) !== normalizeOrigin(config.origin)) {
50
+ throw new Error(`--relay ${flag} disagrees with ${SERVER_CONFIG_FILE} (${config.origin}) — remove one`);
51
+ }
52
+ return flag ?? config?.origin;
53
+ }
54
+
55
+ export { normalizeOrigin, readServerConfig, writeServerConfig, removeServerConfig, readServerBinding, resolveRelayOrigin };
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ joinServer,
4
+ main
5
+ } from "./chunk-gtdpjqa3.js";
6
+ import"./chunk-ncbqa8jk.js";
7
+ import"./chunk-44pnts98.js";
8
+ import"./chunk-t91zt5f0.js";
9
+ import"./chunk-etf7ef97.js";
10
+ import"./chunk-y127fe1a.js";
11
+ import"./chunk-vvj9b1gc.js";
12
+ import"./chunk-jt34g8st.js";
13
+ import"./chunk-kzbzacz9.js";
14
+ import"./chunk-xqycs43g.js";
15
+ import"./chunk-pqe7q1s7.js";
16
+ import"./chunk-ngf1zatm.js";
17
+ import"./chunk-sd5fkxjj.js";
18
+ import"./chunk-df32avw6.js";
19
+ export {
20
+ main,
21
+ joinServer
22
+ };
@@ -0,0 +1,317 @@
1
+ import {
2
+ assertSecureServerOrigin,
3
+ isSecureServerOrigin,
4
+ probeServerAccounts,
5
+ runServerOperation
6
+ } from "./chunk-ncbqa8jk.js";
7
+ import {
8
+ constantValue,
9
+ openHostSettings
10
+ } from "./chunk-44pnts98.js";
11
+ import {
12
+ loadHostKey,
13
+ loadRelayKey
14
+ } from "./chunk-t91zt5f0.js";
15
+ import {
16
+ writeServerConfig
17
+ } from "./chunk-etf7ef97.js";
18
+ import {
19
+ relayHostId
20
+ } from "./chunk-y127fe1a.js";
21
+ import {
22
+ argValue,
23
+ installSpec,
24
+ restartService,
25
+ serviceManager,
26
+ withoutRelayArgs
27
+ } from "./chunk-vvj9b1gc.js";
28
+ import {
29
+ cleanHostName,
30
+ normalizeHostName
31
+ } from "./chunk-pqe7q1s7.js";
32
+ import {
33
+ DEFAULT_DATA_DIR
34
+ } from "./chunk-ngf1zatm.js";
35
+ import {
36
+ commandHint,
37
+ hostEntry
38
+ } from "./chunk-sd5fkxjj.js";
39
+
40
+ // apps/host/src/cli/join.ts
41
+ import { mkdir } from "node:fs/promises";
42
+ import { hostname } from "node:os";
43
+ import { join, resolve as resolve2 } from "node:path";
44
+ import { parseArgs } from "node:util";
45
+
46
+ // apps/host/src/cli/host-link.ts
47
+ import { spawn } from "node:child_process";
48
+ import { arch, platform } from "node:os";
49
+ var SLOW_DOWN_STEP_MS = 5000;
50
+ var REQUEST_TIMEOUT_MS = 15000;
51
+ var EXPIRY_GRACE_MS = 2 * 60000;
52
+ function browserCommand(os, env) {
53
+ if (env.SSH_CONNECTION !== undefined || env.SSH_TTY !== undefined)
54
+ return;
55
+ if (os === "darwin")
56
+ return "open";
57
+ if (os === "linux" && (env.DISPLAY !== undefined || env.WAYLAND_DISPLAY !== undefined))
58
+ return "xdg-open";
59
+ return;
60
+ }
61
+ function openInBrowser(url) {
62
+ const command = browserCommand(process.platform, process.env);
63
+ if (command === undefined)
64
+ return false;
65
+ try {
66
+ const child = spawn(command, [url], { detached: true, stdio: "ignore" });
67
+ child.on("error", () => {});
68
+ child.unref();
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+ function hostOsLabel() {
75
+ const os = platform();
76
+ const name = os === "darwin" ? "macOS" : os === "linux" ? "Linux" : os === "win32" ? "Windows" : os;
77
+ return `${name} (${arch()})`;
78
+ }
79
+ var isRecord = (value) => typeof value === "object" && value !== null;
80
+ var errorCode = (body) => isRecord(body) && typeof body.error === "string" ? body.error : undefined;
81
+ async function requestJoinToken(args, deps) {
82
+ const fetchImpl = deps?.fetch ?? fetch;
83
+ const sleep = deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
84
+ const log = deps?.log ?? ((line) => console.log(line));
85
+ const now = deps?.now ?? Date.now;
86
+ const { origin } = args;
87
+ const timeoutMs = deps?.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
88
+ const post = async (path, payload) => {
89
+ const controller = new AbortController;
90
+ let timer;
91
+ const expired = new Promise((resolve) => {
92
+ timer = setTimeout(() => {
93
+ controller.abort();
94
+ resolve(undefined);
95
+ }, timeoutMs);
96
+ });
97
+ const attempt = (async () => {
98
+ const response = await fetchImpl(`${origin}${path}`, {
99
+ method: "POST",
100
+ headers: { "content-type": "application/json" },
101
+ body: JSON.stringify(payload),
102
+ redirect: "manual",
103
+ signal: controller.signal
104
+ });
105
+ const text = await response.text();
106
+ let body2;
107
+ try {
108
+ body2 = JSON.parse(text);
109
+ } catch {
110
+ body2 = undefined;
111
+ }
112
+ return { status: response.status, body: body2 };
113
+ })().catch(() => {
114
+ return;
115
+ });
116
+ try {
117
+ return await Promise.race([attempt, expired]);
118
+ } finally {
119
+ clearTimeout(timer);
120
+ }
121
+ };
122
+ const started = await post("/api/host-link/start", { hostId: args.hostId, name: args.name, os: args.os ?? hostOsLabel() });
123
+ if (started === undefined)
124
+ throw new Error(`cannot reach ${origin} — check the address and that the server is up`);
125
+ const { body } = started;
126
+ if (started.status !== 200) {
127
+ const code = errorCode(body);
128
+ if (started.status === 404) {
129
+ throw new Error(`${origin} can't confirm hosts in the browser yet — create a join token under "Add a host" and run the join command with it`);
130
+ }
131
+ if (code === "rate_limited")
132
+ throw new Error("too many join requests from this address — wait a few minutes and try again");
133
+ if (code === "busy")
134
+ throw new Error(`${origin} has too many hosts waiting to join right now — try again in a few minutes`);
135
+ throw new Error(`${origin} refused to start the join: ${code ?? `HTTP ${started.status}`}`);
136
+ }
137
+ if (!isRecord(body) || typeof body.deviceCode !== "string" || typeof body.userCode !== "string" || typeof body.verifyUrl !== "string" || typeof body.interval !== "number" || typeof body.expiresAt !== "number" || !isSecureServerOrigin(body.verifyUrl)) {
138
+ throw new Error(`${origin} answered the join request with something unexpected`);
139
+ }
140
+ const { deviceCode, userCode, verifyUrl, expiresAt } = body;
141
+ let intervalMs = Math.max(1, body.interval) * 1000;
142
+ const minutes = Math.max(1, Math.round((expiresAt - now()) / 60000));
143
+ log(`To add this host to your account on ${origin}:`);
144
+ log(` 1. Open ${verifyUrl} and sign in.`);
145
+ log(` 2. Enter this code: ${userCode}`);
146
+ log("");
147
+ log(`Only confirm it if you started this on ${args.name} (host id ${args.hostId}).`);
148
+ const opened = await Promise.resolve((deps?.openBrowser ?? openInBrowser)(verifyUrl)).catch(() => false);
149
+ log(`${opened ? "Opened the page in your browser. " : ""}Waiting for confirmation (the code expires in ${minutes} min)…`);
150
+ for (;; ) {
151
+ await sleep(intervalMs);
152
+ const response = await post("/api/host-link/poll", { deviceCode });
153
+ if (response === undefined || response.status >= 500) {
154
+ if (now() > expiresAt + EXPIRY_GRACE_MS) {
155
+ throw new Error(`the code expired before anyone confirmed it — run the command again for a new one`);
156
+ }
157
+ continue;
158
+ }
159
+ const answer = response.body;
160
+ if (response.status === 200 && isRecord(answer) && typeof answer.token === "string") {
161
+ log("Confirmed.");
162
+ return answer.token;
163
+ }
164
+ const code = response.status === 400 ? errorCode(answer) : undefined;
165
+ if (code === "access_denied")
166
+ throw new Error(`the join was denied on ${origin}`);
167
+ if (code === "expired_token")
168
+ throw new Error(`the code expired before anyone confirmed it — run the command again for a new one`);
169
+ if (code === "slow_down") {
170
+ const asked = isRecord(answer) && typeof answer.interval === "number" ? answer.interval * 1000 : 0;
171
+ intervalMs = Math.max(intervalMs + SLOW_DOWN_STEP_MS, asked);
172
+ } else if (code !== "authorization_pending") {
173
+ throw new Error(`${origin} refused the join: ${code ?? `HTTP ${response.status}`}`);
174
+ }
175
+ }
176
+ }
177
+
178
+ // apps/host/src/cli/service-slot.ts
179
+ import { homedir } from "node:os";
180
+ import { resolve } from "node:path";
181
+ async function openServiceSlot(dataDir, manager) {
182
+ const state = await manager.state().catch(() => ({ installed: false, running: false, detail: "unknown" }));
183
+ const previous = state.installed ? await manager.read() : undefined;
184
+ if (previous !== undefined && resolve(previous.dataDir) !== dataDir) {
185
+ throw new Error(`the installed service uses a different data dir (${previous.dataDir}); ` + `run this again with --data-dir ${previous.dataDir} or uninstall the service first`);
186
+ }
187
+ return { dataDir, manager, state, previous };
188
+ }
189
+ async function writeService(slot, edit, options) {
190
+ const { dataDir, manager, previous } = slot;
191
+ const fresh = installSpec(["--data-dir", dataDir, ...edit([])], {
192
+ execPath: process.execPath,
193
+ env: process.env,
194
+ home: homedir(),
195
+ cwd: process.cwd(),
196
+ mainPath: hostEntry.script,
197
+ entryArgs: hostEntry.args
198
+ });
199
+ if (!slot.state.installed) {
200
+ await manager.install(fresh);
201
+ return "installed";
202
+ }
203
+ const spec = previous === undefined ? fresh : { ...previous, hostArgs: edit(previous.hostArgs) };
204
+ const same = previous !== undefined && spec.hostArgs.join("\x00") === previous.hostArgs.join("\x00");
205
+ if (same && !options.changed && slot.state.running)
206
+ return "unchanged";
207
+ const port = argValue(spec.hostArgs, "--port");
208
+ const code = await restartService({
209
+ force: false,
210
+ argv: ["--data-dir", dataDir, ...port === undefined ? [] : ["--url", `ws://127.0.0.1:${port}`]],
211
+ manager,
212
+ restart: async () => {
213
+ await manager.install(spec);
214
+ }
215
+ });
216
+ return code === 0 ? "updated" : "failed";
217
+ }
218
+
219
+ // apps/host/src/cli/join.ts
220
+ var message = (error) => error instanceof Error ? error.message : String(error);
221
+ async function joinServer(args, deps) {
222
+ const origin = args.origin.replace(/\/+$/, "");
223
+ const dataDir = resolve2(args.dataDir);
224
+ assertSecureServerOrigin(origin);
225
+ if (args.name !== undefined && cleanHostName(args.name) === "") {
226
+ throw new Error(`Invalid host name: ${JSON.stringify(args.name)}`);
227
+ }
228
+ await mkdir(dataDir, { recursive: true });
229
+ const signingKey = await loadRelayKey(dataDir);
230
+ const hostKey = await loadHostKey(dataDir);
231
+ const hostId = relayHostId(signingKey.publicKey);
232
+ const slot = await openServiceSlot(dataDir, deps?.manager ?? serviceManager());
233
+ const probe = await probeServerAccounts(origin, deps?.fetch);
234
+ if (probe === "unreachable")
235
+ throw new Error(`cannot reach ${origin} — check the address and that the server is up`);
236
+ if (probe === "disabled") {
237
+ throw new Error(`${origin} does not run server accounts — the server is too old or was started without a data dir`);
238
+ }
239
+ const settings = await openHostSettings({
240
+ path: join(dataDir, "settings.json"),
241
+ fallback: {
242
+ defaultModel: () => null,
243
+ defaultThinkingLevel: () => null,
244
+ steeringMode: () => "one-at-a-time",
245
+ followUpMode: () => "one-at-a-time"
246
+ },
247
+ models: constantValue([])
248
+ });
249
+ const current = settings.value;
250
+ const storedName = current.custom.includes("hostName") ? current.hostName : undefined;
251
+ const name = normalizeHostName(args.name ?? storedName ?? deps?.hostname ?? hostname());
252
+ const joinToken = args.token ?? await requestJoinToken({ origin, hostId, name }, deps);
253
+ const reply = await runServerOperation({
254
+ origin,
255
+ signingKey,
256
+ noisePublicKey: hostKey.publicKey,
257
+ op: "join",
258
+ joinToken,
259
+ name
260
+ });
261
+ if (reply.t === "refused") {
262
+ throw new Error(reply.reason === "expired_token" ? "the join token expired — generate a fresh one" : reply.reason === "invalid_token" ? "the join token is invalid or already spent — generate a fresh one" : `the server refused the join: ${reply.reason}`);
263
+ }
264
+ if (reply.t !== "joined")
265
+ throw new Error(`unexpected answer to join: ${reply.t}`);
266
+ const username = reply.username;
267
+ let service;
268
+ try {
269
+ await writeServerConfig(dataDir, { origin, username });
270
+ if (args.name !== undefined)
271
+ await settings.update({ hostName: args.name });
272
+ const edit = args.editHostArgs ?? ((hostArgs) => hostArgs);
273
+ service = await writeService(slot, (hostArgs) => edit(withoutRelayArgs(hostArgs)), { changed: true });
274
+ } catch (error) {
275
+ throw new Error(`joined ${username} at ${origin}, but local setup failed: ${message(error)}
276
+ ` + (args.token === undefined ? `run the command again and confirm the new code (re-joining the same host is safe)` : `generate a fresh join token and run the command again (re-joining the same host is safe)`));
277
+ }
278
+ return { hostId, username, origin, service };
279
+ }
280
+ async function main(argv, deps) {
281
+ const { values, positionals } = parseArgs({
282
+ args: [...argv],
283
+ allowPositionals: true,
284
+ options: { "data-dir": { type: "string" }, name: { type: "string" } }
285
+ });
286
+ const [origin, token] = positionals;
287
+ if (origin === undefined || positionals.length > 2) {
288
+ console.error(`usage: ${commandHint("join")} <server-origin> [join-token] [--data-dir DIR] [--name NAME]`);
289
+ console.error(" without a join token, confirm the printed code in the browser where you're signed in");
290
+ process.exit(1);
291
+ }
292
+ try {
293
+ const outcome = await joinServer({
294
+ origin,
295
+ ...token === undefined ? {} : { token },
296
+ dataDir: values["data-dir"] ?? DEFAULT_DATA_DIR,
297
+ ...values.name === undefined ? {} : { name: values.name }
298
+ }, deps);
299
+ console.log(`host id: ${outcome.hostId}`);
300
+ console.log(`joined ${outcome.username} at ${outcome.origin}`);
301
+ if (outcome.service === "installed")
302
+ console.log("service installed and started");
303
+ else if (outcome.service === "updated")
304
+ console.log("service definition updated; host restarted when idle");
305
+ else {
306
+ console.error("joined, but the service restart did not finish — check `service status`");
307
+ process.exitCode = 1;
308
+ }
309
+ } catch (error) {
310
+ console.error(message(error));
311
+ process.exitCode = 1;
312
+ }
313
+ }
314
+ if (false)
315
+ ;
316
+
317
+ export { openServiceSlot, writeService, joinServer, main };
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ main,
4
+ reachabilityLine,
5
+ restartService,
6
+ versionLine
7
+ } from "./chunk-vvj9b1gc.js";
8
+ import"./chunk-jt34g8st.js";
9
+ import"./chunk-kzbzacz9.js";
10
+ import"./chunk-xqycs43g.js";
11
+ import"./chunk-pqe7q1s7.js";
12
+ import"./chunk-ngf1zatm.js";
13
+ import"./chunk-sd5fkxjj.js";
14
+ import"./chunk-df32avw6.js";
15
+ export {
16
+ versionLine,
17
+ restartService,
18
+ reachabilityLine,
19
+ main
20
+ };
@@ -15,7 +15,7 @@ import {
15
15
  organize,
16
16
  parseRtcMaxMessageSize,
17
17
  rtcSelectedPair
18
- } from "./chunk-jgd38rna.js";
18
+ } from "./chunk-xqycs43g.js";
19
19
  import {
20
20
  SECURE_PROLOGUE,
21
21
  SecureSession,
@@ -24,7 +24,7 @@ import {
24
24
  initiateIK,
25
25
  isLoopbackUrl,
26
26
  parseEndpointSet
27
- } from "./chunk-sygeb9qb.js";
27
+ } from "./chunk-pqe7q1s7.js";
28
28
 
29
29
  // packages/protocol/src/remote-durable.ts
30
30
  import { applyImmutable } from "@earendil-works/chord/delta";