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.
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ joinServer,
4
+ openServiceSlot,
5
+ writeService
6
+ } from "./chunk-gtdpjqa3.js";
7
+ import"./chunk-ncbqa8jk.js";
8
+ import"./chunk-44pnts98.js";
9
+ import"./chunk-t91zt5f0.js";
10
+ import {
11
+ normalizeOrigin,
12
+ readServerConfig
13
+ } from "./chunk-etf7ef97.js";
14
+ import"./chunk-y127fe1a.js";
15
+ import {
16
+ argValue,
17
+ serviceManager,
18
+ withoutArg,
19
+ withoutRelayArgs
20
+ } from "./chunk-vvj9b1gc.js";
21
+ import"./chunk-jt34g8st.js";
22
+ import"./chunk-kzbzacz9.js";
23
+ import"./chunk-xqycs43g.js";
24
+ import"./chunk-pqe7q1s7.js";
25
+ import {
26
+ DEFAULT_DATA_DIR
27
+ } from "./chunk-ngf1zatm.js";
28
+ import {
29
+ commandHint
30
+ } from "./chunk-sd5fkxjj.js";
31
+ import"./chunk-df32avw6.js";
32
+
33
+ // apps/host/src/cli/up.ts
34
+ import { resolve } from "node:path";
35
+ import { createInterface } from "node:readline/promises";
36
+ import { parseArgs } from "node:util";
37
+ var OFFICIAL_SERVER_ORIGIN = "https://app.pinomad.si";
38
+ var DEFAULT_REMOTE_PORT = "7422";
39
+ var message = (error) => error instanceof Error ? error.message : String(error);
40
+ function resolveReach(flags, current) {
41
+ if (flags.server === undefined && flags.lan === undefined)
42
+ return;
43
+ if (current !== undefined)
44
+ return { server: flags.server ?? current.server, lan: flags.lan ?? current.lan };
45
+ return { server: flags.server ?? !flags.lan, lan: flags.lan ?? !flags.server };
46
+ }
47
+ async function currentReach(dataDir, deps) {
48
+ const slot = await openServiceSlot(dataDir, deps?.manager ?? serviceManager());
49
+ const config = await readServerConfig(dataDir);
50
+ if (config === undefined && !slot.state.installed)
51
+ return;
52
+ return {
53
+ server: config !== undefined,
54
+ lan: argValue(slot.previous?.hostArgs ?? [], "--remote-port") !== undefined
55
+ };
56
+ }
57
+ function withLan(hostArgs, port) {
58
+ if (port === undefined)
59
+ return withoutArg(withoutArg(hostArgs, "--remote-port"), "--public-url");
60
+ if (argValue(hostArgs, "--remote-port") === port)
61
+ return hostArgs;
62
+ return [...withoutArg(hostArgs, "--remote-port"), "--remote-port", port];
63
+ }
64
+ async function upHost(args, deps) {
65
+ const dataDir = resolve(args.dataDir);
66
+ const { reach } = args;
67
+ if (!reach.server && !reach.lan)
68
+ throw new Error("nothing to set up: choose the server, the LAN, or both");
69
+ const port = args.remotePort;
70
+ if (port !== undefined && !(/^\d+$/.test(port) && Number(port) >= 1 && Number(port) <= 65535)) {
71
+ throw new Error(`--remote-port must be a port number from 1 to 65535, got ${port}`);
72
+ }
73
+ const manager = deps?.manager ?? serviceManager();
74
+ const slot = await openServiceSlot(dataDir, manager);
75
+ const config = await readServerConfig(dataDir);
76
+ const origin = normalizeOrigin(args.origin ?? config?.origin ?? OFFICIAL_SERVER_ORIGIN);
77
+ if (reach.server && config !== undefined && normalizeOrigin(config.origin) !== origin) {
78
+ throw new Error(`this host is joined to ${config.origin} — run \`${commandHint("leave")}\` first to join ${origin}, ` + `or leave out --origin to stay`);
79
+ }
80
+ const lanPort = reach.lan ? args.remotePort ?? argValue(slot.previous?.hostArgs ?? [], "--remote-port") ?? DEFAULT_REMOTE_PORT : undefined;
81
+ const lan = (hostArgs) => withLan(hostArgs, lanPort);
82
+ const notes = [];
83
+ if (reach.server && config === undefined) {
84
+ const joined = await joinServer({ origin, dataDir, editHostArgs: lan }, { ...deps, manager });
85
+ return {
86
+ joined: { origin: joined.origin, username: joined.username, fresh: true },
87
+ ...lanPort === undefined ? {} : { lanPort },
88
+ service: joined.service,
89
+ notes
90
+ };
91
+ }
92
+ const edit = reach.server ? (hostArgs) => lan(withoutRelayArgs(hostArgs)) : lan;
93
+ const service = await writeService(slot, edit, { changed: reach.server });
94
+ if (!reach.server && config !== undefined) {
95
+ notes.push(`still joined to ${config.origin} — run \`${commandHint("leave")}\` to leave it`);
96
+ }
97
+ return {
98
+ ...reach.server && config !== undefined ? { joined: { origin: config.origin, ...config.username === undefined ? {} : { username: config.username }, fresh: false } } : {},
99
+ ...lanPort === undefined ? {} : { lanPort },
100
+ service,
101
+ notes
102
+ };
103
+ }
104
+ async function askOnTerminal(question) {
105
+ const terminal = createInterface({ input: process.stdin, output: process.stdout });
106
+ try {
107
+ return await terminal.question(question);
108
+ } finally {
109
+ terminal.close();
110
+ }
111
+ }
112
+ var CHOICES = [
113
+ { server: true, lan: false },
114
+ { server: false, lan: true },
115
+ { server: true, lan: true }
116
+ ];
117
+ async function askReach(origin, fallback, ask) {
118
+ const preset = CHOICES.findIndex((choice) => choice.server === fallback.server && choice.lan === fallback.lan) + 1;
119
+ let question = `How should your other devices reach this host?
120
+ ` + ` 1) Through ${origin}: from anywhere, with push notifications
121
+ ` + ` 2) Only on your local network and tailnet
122
+ ` + ` 3) Both
123
+ ` + `Choose 1, 2 or 3 [${preset}]: `;
124
+ for (;; ) {
125
+ const answer = (await ask(question)).trim();
126
+ if (answer === "")
127
+ return CHOICES[preset - 1];
128
+ const picked = CHOICES[Number(answer) - 1];
129
+ if (/^[123]$/.test(answer) && picked !== undefined)
130
+ return picked;
131
+ question = "Choose 1, 2 or 3: ";
132
+ }
133
+ }
134
+ var FLAGS = `choose with flags:
135
+ --server [--origin URL] through a PiNomad server (default ${OFFICIAL_SERVER_ORIGIN})
136
+ --lan [--remote-port N] only on the local network and tailnet (default port ${DEFAULT_REMOTE_PORT})
137
+ --server --lan both
138
+ --yes keep what this host has, or the server on a new host`;
139
+ async function main(argv, deps) {
140
+ const { values } = parseArgs({
141
+ args: [...argv],
142
+ allowNegative: true,
143
+ options: {
144
+ server: { type: "boolean" },
145
+ lan: { type: "boolean" },
146
+ yes: { type: "boolean", short: "y" },
147
+ origin: { type: "string" },
148
+ "remote-port": { type: "string" },
149
+ "data-dir": { type: "string" }
150
+ }
151
+ });
152
+ const log = deps?.log ?? ((line) => console.log(line));
153
+ try {
154
+ const dataDir = resolve(values["data-dir"] ?? DEFAULT_DATA_DIR);
155
+ const origin = normalizeOrigin(values.origin ?? (await readServerConfig(dataDir))?.origin ?? OFFICIAL_SERVER_ORIGIN);
156
+ const current = await currentReach(dataDir, deps);
157
+ let reach = resolveReach({ server: values.server, lan: values.lan }, current);
158
+ if (reach === undefined) {
159
+ const fallback = current !== undefined && (current.server || current.lan) ? current : { server: true, lan: false };
160
+ if (values.yes === true) {
161
+ reach = fallback;
162
+ } else if (!(deps?.interactive ?? (process.stdin.isTTY && process.stdout.isTTY))) {
163
+ console.error(`${commandHint("up")}: no terminal to ask how other devices reach this host — ${FLAGS}`);
164
+ process.exitCode = 2;
165
+ return;
166
+ } else {
167
+ reach = await askReach(origin, fallback, deps?.ask ?? askOnTerminal);
168
+ }
169
+ }
170
+ if (!reach.server && !reach.lan) {
171
+ console.error(`${commandHint("up")}: neither the server nor the LAN would be on — ` + `\`${commandHint("service")} install\` runs a host only this machine can reach`);
172
+ process.exitCode = 2;
173
+ return;
174
+ }
175
+ const outcome = await upHost({
176
+ dataDir,
177
+ origin,
178
+ reach,
179
+ ...values["remote-port"] === undefined ? {} : { remotePort: values["remote-port"] }
180
+ }, deps);
181
+ if (outcome.joined !== undefined) {
182
+ const who = outcome.joined.username ?? "an account";
183
+ log(outcome.joined.fresh ? `joined ${who} at ${outcome.joined.origin}` : `already joined ${who} at ${outcome.joined.origin}`);
184
+ log(`Sign in at ${outcome.joined.origin} on your other devices; they pair with this host on their own.`);
185
+ }
186
+ if (outcome.lanPort !== undefined) {
187
+ log(`LAN and tailnet: port ${outcome.lanPort}. Pair a device with \`${commandHint("pair")}\`.`);
188
+ }
189
+ for (const note of outcome.notes)
190
+ log(`note: ${note}`);
191
+ if (outcome.service === "installed")
192
+ log("service installed and started");
193
+ else if (outcome.service === "updated")
194
+ log("service updated; host restarted when idle");
195
+ else if (outcome.service === "unchanged")
196
+ log("service already running with these settings");
197
+ else {
198
+ console.error(`the service restart did not finish — check \`${commandHint("status")}\``);
199
+ process.exitCode = 1;
200
+ }
201
+ } catch (error) {
202
+ console.error(message(error));
203
+ process.exitCode = 1;
204
+ }
205
+ }
206
+ if (false)
207
+ ;
208
+ export {
209
+ upHost,
210
+ resolveReach,
211
+ main,
212
+ OFFICIAL_SERVER_ORIGIN,
213
+ DEFAULT_REMOTE_PORT
214
+ };
@@ -6,10 +6,10 @@ import {
6
6
  relayWsBase,
7
7
  signRelayChallenge,
8
8
  signRelayChallengeV2
9
- } from "./chunk-khcpv54k.js";
9
+ } from "./chunk-y127fe1a.js";
10
10
  import {
11
11
  toBase64Url
12
- } from "./chunk-sygeb9qb.js";
12
+ } from "./chunk-pqe7q1s7.js";
13
13
 
14
14
  // apps/host/src/cli/server-ops.ts
15
15
  import { WebSocket } from "ws";
@@ -55,6 +55,7 @@ function isSecureServerOrigin(origin) {
55
55
  const loopback = host === "localhost" || host.endsWith(".localhost") || ipv4Loopback || host === "::1" || host === "[::1]";
56
56
  return url.protocol === "https:" || url.protocol === "http:" && loopback;
57
57
  }
58
+ var DEVICE_HOST_STATES = new Set(["registered", "revoking", "revoked"]);
58
59
 
59
60
  // apps/host/src/cli/server-ops.ts
60
61
  function assertSecureServerOrigin(origin) {
@@ -122,11 +123,11 @@ async function runServerOperation(options) {
122
123
  finish(() => reject(new Error(`${origin} ignored the operation — the server is too old for accounts; upgrade it first`)));
123
124
  return;
124
125
  }
125
- if (frame.t === "incoming" || frame.t === "pairing-request" || frame.t === "push-result")
126
+ if (frame.t === "incoming" || frame.t === "pairing-request" || frame.t === "push-result" || frame.t === "revoke-device")
126
127
  return;
127
128
  finish(() => resolve(frame));
128
129
  });
129
130
  });
130
131
  }
131
132
 
132
- export { assertSecureServerOrigin, probeServerAccounts, runServerOperation };
133
+ export { isSecureServerOrigin, assertSecureServerOrigin, probeServerAccounts, runServerOperation };
@@ -427,4 +427,4 @@ function concatParts(parts) {
427
427
  return out;
428
428
  }
429
429
 
430
- export { ENDPOINT_SET_SCHEMA_VERSION, MAX_HOST_ENDPOINT_URL_LENGTH, isLoopbackUrl, parseEndpointSet, encodeEndpointSetParam, HOST_SETTING_KEYS, THINKING_LEVELS, isThinkingLevel, isQueueMode, HOST_NAME_MAX_UNITS, cleanHostName, normalizeHostName, generateKeyPair, keyPairFromPrivate, initiateIK, respondIK, SECURE_PROLOGUE, SIGNAL_PROLOGUE, encodeClientHello, encodeHostHello, decodeClientHello, decodeHostHello, toBase64Url, fromBase64Url, SecureSession };
430
+ export { ENDPOINT_SET_SCHEMA_VERSION, MAX_HOST_ENDPOINT_URL_LENGTH, isLoopbackUrl, parseEndpointSet, encodeEndpointSetParam, generateKeyPair, keyPairFromPrivate, initiateIK, respondIK, SECURE_PROLOGUE, SIGNAL_PROLOGUE, encodeClientHello, encodeHostHello, decodeClientHello, decodeHostHello, toBase64Url, fromBase64Url, SecureSession, HOST_SETTING_KEYS, THINKING_LEVELS, isThinkingLevel, isQueueMode, HOST_NAME_MAX_UNITS, cleanHostName, normalizeHostName };
@@ -1,6 +1,6 @@
1
1
  // apps/host/src/distribution.ts
2
2
  import { fileURLToPath } from "node:url";
3
- var packagedVersion = "0.9.1";
3
+ var packagedVersion = "0.11.0";
4
4
  var webRoot = fileURLToPath(new URL(packagedVersion === undefined ? "../../web/dist" : "../web/", import.meta.url));
5
5
  var hostEntry = packagedVersion === undefined ? { script: fileURLToPath(new URL("./main.ts", import.meta.url)), args: [] } : { script: fileURLToPath(new URL("./pinomad.js", import.meta.url)), args: ["host"] };
6
6
  var commandHint = (name) => packagedVersion === undefined ? `bun run ${name}` : `pinomad ${name}`;
@@ -3,7 +3,7 @@ import {
3
3
  generateKeyPair,
4
4
  keyPairFromPrivate,
5
5
  toBase64Url
6
- } from "./chunk-sygeb9qb.js";
6
+ } from "./chunk-pqe7q1s7.js";
7
7
 
8
8
  // apps/host/src/devices.ts
9
9
  import { link, readFile, unlink, writeFile } from "node:fs/promises";
@@ -52,7 +52,7 @@ async function admitDevice(harness, input, context) {
52
52
  if (revocations.revoked.includes(input.publicKey)) {
53
53
  revocations.revoked = revocations.revoked.filter((key) => key !== input.publicKey);
54
54
  }
55
- return { outcome: "admitted", entry };
55
+ return { outcome: "admitted", entry, ...offer.requestId === undefined ? {} : { requestId: offer.requestId } };
56
56
  }, context);
57
57
  }
58
58
  async function loadHostKey(dataDir) {
@@ -117,7 +117,7 @@ function pairingOffers(ttlMs = 5 * 60 * 1000) {
117
117
  };
118
118
  return {
119
119
  create: (source) => mint({ source }),
120
- createAccount: (binding) => mint({ source: "account", binding }),
120
+ createAccount: (binding, requestId) => mint({ source: "account", binding, ...requestId === undefined ? {} : { requestId } }),
121
121
  consume(secret) {
122
122
  prune();
123
123
  const candidate = Buffer.from(secret);
@@ -140,53 +140,4 @@ function pairingOffers(ttlMs = 5 * 60 * 1000) {
140
140
  };
141
141
  }
142
142
 
143
- // apps/host/src/server-config.ts
144
- import { randomBytes as randomBytes2 } from "node:crypto";
145
- import { readFile as readFile2, rename, rm, writeFile as writeFile2 } from "node:fs/promises";
146
- import { join as join2 } from "node:path";
147
- var SERVER_CONFIG_FILE = "server.json";
148
- var normalizeOrigin = (origin) => origin.replace(/\/+$/, "");
149
- async function readServerConfig(dataDir) {
150
- let text;
151
- try {
152
- text = await readFile2(join2(dataDir, SERVER_CONFIG_FILE), "utf8");
153
- } catch (error) {
154
- if (error.code === "ENOENT")
155
- return;
156
- throw error;
157
- }
158
- let value;
159
- try {
160
- value = JSON.parse(text);
161
- } catch {
162
- throw new Error(`${join2(dataDir, SERVER_CONFIG_FILE)} is not valid JSON — delete it or run \`pinomad leave\``);
163
- }
164
- const origin = value?.origin;
165
- if (typeof origin !== "string" || origin.trim() === "") {
166
- throw new Error(`${join2(dataDir, SERVER_CONFIG_FILE)} is missing a string "origin"`);
167
- }
168
- const binding = value.binding;
169
- return {
170
- origin,
171
- ...typeof binding === "string" && binding !== "" ? { binding } : {}
172
- };
173
- }
174
- async function writeServerConfig(dataDir, config) {
175
- const path = join2(dataDir, SERVER_CONFIG_FILE);
176
- const tmp = `${path}.tmp-${process.pid}`;
177
- await writeFile2(tmp, `${JSON.stringify({ origin: normalizeOrigin(config.origin), binding: randomBytes2(16).toString("base64url") })}
178
- `, { mode: 384 });
179
- await rename(tmp, path);
180
- }
181
- var removeServerConfig = (dataDir) => rm(join2(dataDir, SERVER_CONFIG_FILE), { force: true });
182
- async function readServerBinding(dataDir) {
183
- return (await readServerConfig(dataDir))?.binding;
184
- }
185
- function resolveRelayOrigin(flag, config) {
186
- if (flag !== undefined && config !== undefined && normalizeOrigin(flag) !== normalizeOrigin(config.origin)) {
187
- throw new Error(`--relay ${flag} disagrees with ${SERVER_CONFIG_FILE} (${config.origin}) — remove one`);
188
- }
189
- return flag ?? config?.origin;
190
- }
191
-
192
- export { DevicesDoc, ensureDevices, RevocationsDoc, admitDevice, loadHostKey, loadRelayKey, pairingOffers, normalizeOrigin, readServerConfig, writeServerConfig, removeServerConfig, readServerBinding, resolveRelayOrigin };
143
+ export { DevicesDoc, ensureDevices, RevocationsDoc, admitDevice, loadHostKey, loadRelayKey, pairingOffers };
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  connectRemoteDurable
3
- } from "./chunk-4sepcfpy.js";
4
- import"./chunk-jgd38rna.js";
3
+ } from "./chunk-jt34g8st.js";
4
+ import"./chunk-xqycs43g.js";
5
5
  import {
6
6
  isLoopbackUrl
7
- } from "./chunk-sygeb9qb.js";
7
+ } from "./chunk-pqe7q1s7.js";
8
8
  import {
9
9
  hostAddress
10
10
  } from "./chunk-ngf1zatm.js";
@@ -1,19 +1,19 @@
1
- import {
2
- isParseArgsError
3
- } from "./chunk-kzbzacz9.js";
4
- import {
5
- commandHint,
6
- hostEntry,
7
- packagedVersion
8
- } from "./chunk-fqgvf8v0.js";
9
1
  import {
10
2
  ProtocolMismatchError,
11
3
  connectRemoteDurable
12
- } from "./chunk-4sepcfpy.js";
4
+ } from "./chunk-jt34g8st.js";
5
+ import {
6
+ isParseArgsError
7
+ } from "./chunk-kzbzacz9.js";
13
8
  import {
14
9
  DEFAULT_DATA_DIR,
15
10
  hostAddress
16
11
  } from "./chunk-ngf1zatm.js";
12
+ import {
13
+ commandHint,
14
+ hostEntry,
15
+ packagedVersion
16
+ } from "./chunk-sd5fkxjj.js";
17
17
 
18
18
  // apps/host/src/cli/service.ts
19
19
  import { execFile as execFile2 } from "node:child_process";
@@ -81,20 +81,21 @@ function argValue(args, flag) {
81
81
  }
82
82
  return value;
83
83
  }
84
- function withoutRelayArgs(args) {
84
+ function withoutArg(args, flag) {
85
85
  const out = [];
86
86
  for (let i = 0;i < args.length; i += 1) {
87
87
  const arg = args[i];
88
- if (arg === "--relay") {
88
+ if (arg === flag) {
89
89
  i += 1;
90
90
  continue;
91
91
  }
92
- if (arg.startsWith("--relay="))
92
+ if (arg.startsWith(`${flag}=`))
93
93
  continue;
94
94
  out.push(arg);
95
95
  }
96
96
  return out;
97
97
  }
98
+ var withoutRelayArgs = (args) => withoutArg(args, "--relay");
98
99
  var unescapeExec = (value) => value.replace(/\\(.)/g, "$1");
99
100
  var unspecify = (value) => value.replace(/%%/g, "%").replace(/\$\$/g, "$");
100
101
  var xmlUnescape = (value) => value.replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
@@ -409,15 +410,13 @@ async function install(argv) {
409
410
  console.log(`data dir: ${spec.dataDir}`);
410
411
  console.log(`browser link: ${commandHint("link")} pairing QR: ${commandHint("pair")}`);
411
412
  }
412
- async function status(argv) {
413
- const state = await serviceManager().state();
414
- console.log(`service: ${state.installed ? state.detail : "not installed"}`);
415
- if (packagedVersion === undefined) {
416
- const head = await run2("git", ["rev-parse", "--short", "HEAD"], { cwd: repoRoot }).then(({ stdout }) => stdout.trim(), () => "unknown");
417
- console.log(`checkout: ${head}`);
418
- } else {
419
- console.log(`version: ${packagedVersion}`);
420
- }
413
+ async function versionLine() {
414
+ if (packagedVersion !== undefined)
415
+ return `version: ${packagedVersion}`;
416
+ const head = await run2("git", ["rev-parse", "--short", "HEAD"], { cwd: repoRoot }).then(({ stdout }) => stdout.trim(), () => "unknown");
417
+ return `checkout: ${head}`;
418
+ }
419
+ async function reachabilityLine(argv) {
421
420
  try {
422
421
  const { url, token } = await hostAddress(argv);
423
422
  const remote = await connectToHost(url, token);
@@ -425,14 +424,20 @@ async function status(argv) {
425
424
  await remote.controller.toggleTasks();
426
425
  const tasks = remote.view.current().tasks;
427
426
  const count = tasks === undefined ? 0 : Object.keys(tasks.tasks).length;
428
- console.log(`host: reachable at ${url}, ${count} live task${count === 1 ? "" : "s"}`);
429
427
  remote.close();
428
+ return `host: reachable at ${url}, ${count} live task${count === 1 ? "" : "s"}`;
430
429
  } catch (error) {
431
430
  if (isParseArgsError(error))
432
431
  throw error;
433
- console.log(`host: unreachable (${message(error)})`);
432
+ return `host: unreachable (${message(error)})`;
434
433
  }
435
434
  }
435
+ async function status(argv) {
436
+ const state = await serviceManager().state();
437
+ console.log(`service: ${state.installed ? state.detail : "not installed"}`);
438
+ console.log(await versionLine());
439
+ console.log(await reachabilityLine(argv));
440
+ }
436
441
  async function restartService({
437
442
  force,
438
443
  argv,
@@ -536,4 +541,4 @@ async function main(argv) {
536
541
  }
537
542
  if (false) {}
538
543
 
539
- export { installSpec, argValue, withoutRelayArgs, serviceManager, restartService, main };
544
+ export { installSpec, argValue, withoutArg, withoutRelayArgs, serviceManager, versionLine, reachabilityLine, restartService, main };