pinomad 0.3.0 → 0.4.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.
- package/dist/{chunk-n60kkf1m.js → chunk-17p6bfvy.js} +4 -4
- package/dist/chunk-302y6by9.js +50 -0
- package/dist/{chunk-bggesvnj.js → chunk-374zqx5h.js} +15 -92
- package/dist/chunk-5785kwej.js +128 -0
- package/dist/{chunk-z4x88s0q.js → chunk-7gmkgyaz.js} +76 -3
- package/dist/chunk-837gh8nr.js +138 -0
- package/dist/{chunk-t9y2wfk5.js → chunk-akgxwg8f.js} +98 -6
- package/dist/chunk-b3f0ced1.js +147 -0
- package/dist/{chunk-syf0z68f.js → chunk-d4y99pfy.js} +2 -2
- package/dist/{chunk-3tf23vjq.js → chunk-fgx2sh7d.js} +1 -1
- package/dist/chunk-knaw1a9g.js +182 -0
- package/dist/{chunk-c155p0t0.js → chunk-myjp910s.js} +106 -7
- package/dist/{chunk-3qn8gbjb.js → chunk-n73t5xzw.js} +1 -1
- package/dist/{chunk-kxws8b83.js → chunk-prhvksh3.js} +4 -4
- package/dist/chunk-vw5yb5pd.js +1123 -0
- package/dist/chunk-xhwkx9bd.js +139 -0
- package/dist/{chunk-7fqn69ys.js → chunk-xrvmnymc.js} +2 -2
- package/dist/{chunk-tfqc1djy.js → chunk-z5pmcscx.js} +530 -242
- package/dist/pinomad.js +14 -9
- package/package.json +1 -1
- package/web/assets/index-8mNNPXPT.js +383 -0
- package/web/assets/{index-CG33GBxM.css → index-BFJJgGGo.css} +1 -1
- package/web/index.html +2 -2
- package/dist/chunk-4dj2vb8t.js +0 -284
- package/dist/chunk-fxaewke2.js +0 -113
- package/web/assets/index-CBXdQLua.js +0 -383
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
commandHint,
|
|
3
3
|
hostEntry,
|
|
4
4
|
packagedVersion
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-fgx2sh7d.js";
|
|
6
6
|
import {
|
|
7
7
|
connectRemoteDurable
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-374zqx5h.js";
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_DATA_DIR,
|
|
11
11
|
hostAddress
|
|
@@ -66,6 +66,86 @@ function installSpec(argv, inputs) {
|
|
|
66
66
|
}
|
|
67
67
|
var launchdPlistPath = (home) => join(home, "Library", "LaunchAgents", "pinomad.host.plist");
|
|
68
68
|
var systemdUnitPath = (home) => join(home, ".config", "systemd", "user", "pinomad.service");
|
|
69
|
+
function argValue(args, flag) {
|
|
70
|
+
let value;
|
|
71
|
+
for (let i = 0;i < args.length; i += 1) {
|
|
72
|
+
const arg = args[i];
|
|
73
|
+
if (arg === flag)
|
|
74
|
+
value = args[i + 1];
|
|
75
|
+
else if (arg.startsWith(`${flag}=`))
|
|
76
|
+
value = arg.slice(flag.length + 1);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function withoutRelayArgs(args) {
|
|
81
|
+
const out = [];
|
|
82
|
+
for (let i = 0;i < args.length; i += 1) {
|
|
83
|
+
const arg = args[i];
|
|
84
|
+
if (arg === "--relay") {
|
|
85
|
+
i += 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (arg.startsWith("--relay="))
|
|
89
|
+
continue;
|
|
90
|
+
out.push(arg);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
var unescapeExec = (value) => value.replace(/\\(.)/g, "$1");
|
|
95
|
+
var unspecify = (value) => value.replace(/%%/g, "%").replace(/\$\$/g, "$");
|
|
96
|
+
var xmlUnescape = (value) => value.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
97
|
+
function specFromProgram(program, env, home) {
|
|
98
|
+
const at = program.indexOf("--data-dir");
|
|
99
|
+
if (program.length < 4 || at < 2 || program[at + 1] === undefined) {
|
|
100
|
+
throw new Error("service definition has no --data-dir argument boundary");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
nodePath: program[0],
|
|
104
|
+
mainPath: program[1],
|
|
105
|
+
entryArgs: program.slice(2, at),
|
|
106
|
+
dataDir: program[at + 1],
|
|
107
|
+
hostArgs: program.slice(at + 2),
|
|
108
|
+
env: {
|
|
109
|
+
PATH: env.PATH ?? "",
|
|
110
|
+
...env.LANG === undefined ? {} : { LANG: env.LANG },
|
|
111
|
+
...env.LC_ALL === undefined ? {} : { LC_ALL: env.LC_ALL }
|
|
112
|
+
},
|
|
113
|
+
home
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function specFromSystemdUnit(text) {
|
|
117
|
+
const exec = /^ExecStart=(.*)$/m.exec(text)?.[1];
|
|
118
|
+
if (exec === undefined)
|
|
119
|
+
throw new Error("service unit has no ExecStart");
|
|
120
|
+
const program = [...exec.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => unspecify(unescapeExec(match[1])));
|
|
121
|
+
const env = {};
|
|
122
|
+
const envLine = /^Environment=(.*)$/m.exec(text)?.[1] ?? "";
|
|
123
|
+
for (const match of envLine.matchAll(/"([^"]*)"/g)) {
|
|
124
|
+
const pair = unspecify(match[1]);
|
|
125
|
+
const eq = pair.indexOf("=");
|
|
126
|
+
if (eq > 0)
|
|
127
|
+
env[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
128
|
+
}
|
|
129
|
+
const workdir = /^WorkingDirectory=(.*)$/m.exec(text)?.[1];
|
|
130
|
+
if (workdir === undefined)
|
|
131
|
+
throw new Error("service unit has no WorkingDirectory");
|
|
132
|
+
return specFromProgram(program, env, unspecify(workdir));
|
|
133
|
+
}
|
|
134
|
+
function specFromLaunchdPlist(text) {
|
|
135
|
+
const array = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(text)?.[1];
|
|
136
|
+
if (array === undefined)
|
|
137
|
+
throw new Error("plist has no ProgramArguments");
|
|
138
|
+
const program = [...array.matchAll(/<string>([\s\S]*?)<\/string>/g)].map((match) => xmlUnescape(match[1]));
|
|
139
|
+
const env = {};
|
|
140
|
+
const dict = /<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/.exec(text)?.[1] ?? "";
|
|
141
|
+
for (const match of dict.matchAll(/<key>([\s\S]*?)<\/key>\s*<string>([\s\S]*?)<\/string>/g)) {
|
|
142
|
+
env[xmlUnescape(match[1])] = xmlUnescape(match[2]);
|
|
143
|
+
}
|
|
144
|
+
const workdir = /<key>WorkingDirectory<\/key>\s*<string>([\s\S]*?)<\/string>/.exec(text)?.[1];
|
|
145
|
+
if (workdir === undefined)
|
|
146
|
+
throw new Error("plist has no WorkingDirectory");
|
|
147
|
+
return specFromProgram(program, env, xmlUnescape(workdir));
|
|
148
|
+
}
|
|
69
149
|
var xmlEscape = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
70
150
|
function launchdPlist(spec) {
|
|
71
151
|
const program = [spec.nodePath, spec.mainPath, ...spec.entryArgs, "--data-dir", spec.dataDir, ...spec.hostArgs];
|
|
@@ -214,6 +294,10 @@ var launchd = {
|
|
|
214
294
|
process.stdout.write(`${all.slice(-lines).join(`
|
|
215
295
|
`)}
|
|
216
296
|
`);
|
|
297
|
+
},
|
|
298
|
+
async read() {
|
|
299
|
+
const text = await readFile(launchdPlistPath(homedir()), "utf8").then((value) => value, (error) => error.code === "ENOENT" ? undefined : Promise.reject(error));
|
|
300
|
+
return text === undefined ? undefined : specFromLaunchdPlist(text);
|
|
217
301
|
}
|
|
218
302
|
};
|
|
219
303
|
var systemd = {
|
|
@@ -273,6 +357,10 @@ var systemd = {
|
|
|
273
357
|
stdio: "inherit"
|
|
274
358
|
});
|
|
275
359
|
await new Promise((resolve2) => journal.once("exit", () => resolve2()));
|
|
360
|
+
},
|
|
361
|
+
async read() {
|
|
362
|
+
const text = await readFile(systemdUnitPath(homedir()), "utf8").then((value) => value, (error) => error.code === "ENOENT" ? undefined : Promise.reject(error));
|
|
363
|
+
return text === undefined ? undefined : specFromSystemdUnit(text);
|
|
276
364
|
}
|
|
277
365
|
};
|
|
278
366
|
function serviceManager(platform = process.platform) {
|
|
@@ -339,8 +427,12 @@ async function status(argv) {
|
|
|
339
427
|
console.log(`host: unreachable (${message(error)})`);
|
|
340
428
|
}
|
|
341
429
|
}
|
|
342
|
-
async function restartService({
|
|
343
|
-
|
|
430
|
+
async function restartService({
|
|
431
|
+
force,
|
|
432
|
+
argv,
|
|
433
|
+
manager = serviceManager(),
|
|
434
|
+
restart
|
|
435
|
+
}) {
|
|
344
436
|
if (!(await manager.state()).installed) {
|
|
345
437
|
console.log(`service not installed (${commandHint("service")} install)`);
|
|
346
438
|
return 1;
|
|
@@ -360,7 +452,7 @@ async function restartService({ force, argv }) {
|
|
|
360
452
|
remote.close();
|
|
361
453
|
} catch {}
|
|
362
454
|
}
|
|
363
|
-
await manager.restart();
|
|
455
|
+
await (restart?.() ?? manager.restart());
|
|
364
456
|
if (address === undefined) {
|
|
365
457
|
console.log("restart requested (host address unknown; not polling)");
|
|
366
458
|
return 0;
|
|
@@ -427,4 +519,4 @@ async function main(argv) {
|
|
|
427
519
|
if (false)
|
|
428
520
|
;
|
|
429
521
|
|
|
430
|
-
export { serviceManager, restartService, main };
|
|
522
|
+
export { installSpec, argValue, withoutRelayArgs, serviceManager, restartService, main };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
assertSecureServerOrigin,
|
|
4
|
+
probeServerAccounts,
|
|
5
|
+
runServerOperation
|
|
6
|
+
} from "./chunk-5785kwej.js";
|
|
7
|
+
import {
|
|
8
|
+
writeServerConfig
|
|
9
|
+
} from "./chunk-302y6by9.js";
|
|
10
|
+
import"./chunk-knaw1a9g.js";
|
|
11
|
+
import {
|
|
12
|
+
argValue,
|
|
13
|
+
installSpec,
|
|
14
|
+
restartService,
|
|
15
|
+
serviceManager,
|
|
16
|
+
withoutRelayArgs
|
|
17
|
+
} from "./chunk-akgxwg8f.js";
|
|
18
|
+
import {
|
|
19
|
+
commandHint,
|
|
20
|
+
hostEntry
|
|
21
|
+
} from "./chunk-fgx2sh7d.js";
|
|
22
|
+
import"./chunk-374zqx5h.js";
|
|
23
|
+
import"./chunk-myjp910s.js";
|
|
24
|
+
import {
|
|
25
|
+
loadHostKey,
|
|
26
|
+
loadRelayKey
|
|
27
|
+
} from "./chunk-837gh8nr.js";
|
|
28
|
+
import {
|
|
29
|
+
relayHostId
|
|
30
|
+
} from "./chunk-7gmkgyaz.js";
|
|
31
|
+
import {
|
|
32
|
+
DEFAULT_DATA_DIR
|
|
33
|
+
} from "./chunk-cjfmfyjb.js";
|
|
34
|
+
import"./chunk-df32avw6.js";
|
|
35
|
+
|
|
36
|
+
// apps/host/src/cli/join.ts
|
|
37
|
+
import { mkdir } from "node:fs/promises";
|
|
38
|
+
import { homedir, hostname } from "node:os";
|
|
39
|
+
import { resolve } from "node:path";
|
|
40
|
+
import { parseArgs } from "node:util";
|
|
41
|
+
var message = (error) => error instanceof Error ? error.message : String(error);
|
|
42
|
+
async function joinServer(args, deps) {
|
|
43
|
+
const origin = args.origin.replace(/\/+$/, "");
|
|
44
|
+
const dataDir = resolve(args.dataDir);
|
|
45
|
+
assertSecureServerOrigin(origin);
|
|
46
|
+
await mkdir(dataDir, { recursive: true });
|
|
47
|
+
const signingKey = await loadRelayKey(dataDir);
|
|
48
|
+
const hostKey = await loadHostKey(dataDir);
|
|
49
|
+
const hostId = relayHostId(signingKey.publicKey);
|
|
50
|
+
const manager = deps?.manager ?? serviceManager();
|
|
51
|
+
const installed = await manager.state().then((state) => state.installed, () => false);
|
|
52
|
+
const previous = installed ? await manager.read() : undefined;
|
|
53
|
+
if (previous !== undefined && resolve(previous.dataDir) !== dataDir) {
|
|
54
|
+
throw new Error(`the installed service uses a different data dir (${previous.dataDir}); ` + `join it with --data-dir ${previous.dataDir} or uninstall the service first`);
|
|
55
|
+
}
|
|
56
|
+
const probe = await probeServerAccounts(origin, deps?.fetch);
|
|
57
|
+
if (probe === "unreachable")
|
|
58
|
+
throw new Error(`cannot reach ${origin} — check the address and that the server is up`);
|
|
59
|
+
if (probe === "disabled") {
|
|
60
|
+
throw new Error(`${origin} does not run server accounts — the server is too old or was started without a data dir`);
|
|
61
|
+
}
|
|
62
|
+
const reply = await runServerOperation({
|
|
63
|
+
origin,
|
|
64
|
+
signingKey,
|
|
65
|
+
noisePublicKey: hostKey.publicKey,
|
|
66
|
+
op: "join",
|
|
67
|
+
joinToken: args.token,
|
|
68
|
+
name: args.name ?? deps?.hostname ?? hostname()
|
|
69
|
+
});
|
|
70
|
+
if (reply.t === "refused") {
|
|
71
|
+
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}`);
|
|
72
|
+
}
|
|
73
|
+
if (reply.t !== "joined")
|
|
74
|
+
throw new Error(`unexpected answer to join: ${reply.t}`);
|
|
75
|
+
const username = reply.username;
|
|
76
|
+
let service;
|
|
77
|
+
try {
|
|
78
|
+
await writeServerConfig(dataDir, { origin });
|
|
79
|
+
const freshSpec = installSpec(["--data-dir", dataDir], {
|
|
80
|
+
execPath: process.execPath,
|
|
81
|
+
env: process.env,
|
|
82
|
+
home: homedir(),
|
|
83
|
+
cwd: process.cwd(),
|
|
84
|
+
mainPath: hostEntry.script,
|
|
85
|
+
entryArgs: hostEntry.args
|
|
86
|
+
});
|
|
87
|
+
if (!installed) {
|
|
88
|
+
await manager.install(freshSpec);
|
|
89
|
+
service = "installed";
|
|
90
|
+
} else {
|
|
91
|
+
const spec = previous === undefined ? freshSpec : { ...previous, hostArgs: withoutRelayArgs(previous.hostArgs) };
|
|
92
|
+
const port = argValue(spec.hostArgs, "--port");
|
|
93
|
+
const code = await restartService({
|
|
94
|
+
force: false,
|
|
95
|
+
argv: ["--data-dir", dataDir, ...port === undefined ? [] : ["--url", `ws://127.0.0.1:${port}`]],
|
|
96
|
+
manager,
|
|
97
|
+
restart: async () => {
|
|
98
|
+
await manager.install(spec);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
service = code === 0 ? "updated" : "failed";
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new Error(`joined ${username} at ${origin}, but local setup failed: ${message(error)}
|
|
105
|
+
` + `generate a fresh join token and run the command again (re-joining the same host is safe)`);
|
|
106
|
+
}
|
|
107
|
+
return { hostId, username, origin, service };
|
|
108
|
+
}
|
|
109
|
+
async function main(argv) {
|
|
110
|
+
const { values, positionals } = parseArgs({
|
|
111
|
+
args: [...argv],
|
|
112
|
+
allowPositionals: true,
|
|
113
|
+
options: { "data-dir": { type: "string" }, name: { type: "string" } }
|
|
114
|
+
});
|
|
115
|
+
const [origin, token] = positionals;
|
|
116
|
+
if (origin === undefined || token === undefined) {
|
|
117
|
+
console.error(`usage: ${commandHint("join")} <server-origin> <join-token> [--data-dir DIR] [--name NAME]`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const outcome = await joinServer({
|
|
122
|
+
origin,
|
|
123
|
+
token,
|
|
124
|
+
dataDir: values["data-dir"] ?? DEFAULT_DATA_DIR,
|
|
125
|
+
...values.name === undefined ? {} : { name: values.name }
|
|
126
|
+
});
|
|
127
|
+
console.log(`host id: ${outcome.hostId}`);
|
|
128
|
+
console.log(`joined ${outcome.username} at ${outcome.origin}`);
|
|
129
|
+
if (outcome.service === "installed")
|
|
130
|
+
console.log("service installed and started");
|
|
131
|
+
else if (outcome.service === "updated")
|
|
132
|
+
console.log("service definition updated; host restarted when idle");
|
|
133
|
+
else {
|
|
134
|
+
console.error("joined, but the service restart did not finish — check `service status`");
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(message(error));
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (false)
|
|
143
|
+
;
|
|
144
|
+
export {
|
|
145
|
+
main,
|
|
146
|
+
joinServer
|
|
147
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// apps/host/src/distribution.ts
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
var packagedVersion = "0.
|
|
3
|
+
var packagedVersion = "0.4.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}`;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RELAY_CLOSE,
|
|
3
|
+
decodeRelayToHost,
|
|
4
|
+
encodeRelayFrame,
|
|
5
|
+
relayHostId,
|
|
6
|
+
signRelayChallenge,
|
|
7
|
+
signRelayChallengeV2,
|
|
8
|
+
toBase64Url
|
|
9
|
+
} from "./chunk-7gmkgyaz.js";
|
|
10
|
+
|
|
11
|
+
// apps/host/src/relay-link.ts
|
|
12
|
+
import { WebSocket } from "ws";
|
|
13
|
+
function relayWsBase(origin) {
|
|
14
|
+
const normalized = origin.replace(/\/+$/, "");
|
|
15
|
+
if (normalized.startsWith("https://"))
|
|
16
|
+
return `wss://${normalized.slice("https://".length)}`;
|
|
17
|
+
if (normalized.startsWith("http://"))
|
|
18
|
+
return `ws://${normalized.slice("http://".length)}`;
|
|
19
|
+
throw new Error(`relay origin must be an http(s) URL: ${origin}`);
|
|
20
|
+
}
|
|
21
|
+
var REJECTION_REASONS = {
|
|
22
|
+
[RELAY_CLOSE.badSignature]: () => `the relay rejected this host's registration signature — a version mismatch?`,
|
|
23
|
+
[RELAY_CLOSE.hostNotAllowed]: (hostId) => `the server does not allow this host; join it to an account (\`pinomad join\`) or add --allow-host ${hostId} to the server's arguments`,
|
|
24
|
+
[RELAY_CLOSE.replaced]: () => `the relay replaced this control connection — another process registered the same hostId`
|
|
25
|
+
};
|
|
26
|
+
function startRelayLink(options) {
|
|
27
|
+
const origin = options.origin.replace(/\/+$/, "");
|
|
28
|
+
const wsBase = relayWsBase(origin);
|
|
29
|
+
const hostId = relayHostId(options.signingKey.publicKey);
|
|
30
|
+
const minDelay = options.reconnectDelayMs?.min ?? 1000;
|
|
31
|
+
const maxDelay = options.reconnectDelayMs?.max ?? 30000;
|
|
32
|
+
const maxPendingAccepts = options.maxPendingAccepts ?? 16;
|
|
33
|
+
let state = { status: "connecting" };
|
|
34
|
+
let stopped = false;
|
|
35
|
+
let control;
|
|
36
|
+
let delay = minDelay;
|
|
37
|
+
let retryTimer;
|
|
38
|
+
let pendingAccepts = 0;
|
|
39
|
+
function setState(next) {
|
|
40
|
+
const changed = state.status !== next.status || state.status === "rejected" && next.status === "rejected" && state.reason !== next.reason;
|
|
41
|
+
state = next;
|
|
42
|
+
if (changed)
|
|
43
|
+
options.onStateChange?.(next);
|
|
44
|
+
}
|
|
45
|
+
const watched = new Set;
|
|
46
|
+
const ponged = new WeakMap;
|
|
47
|
+
const pingIntervalMs = options.pingIntervalMs ?? 30000;
|
|
48
|
+
const pingTimer = setInterval(() => {
|
|
49
|
+
for (const socket of watched) {
|
|
50
|
+
if (socket.readyState !== WebSocket.OPEN)
|
|
51
|
+
continue;
|
|
52
|
+
if (ponged.get(socket) === true) {
|
|
53
|
+
ponged.set(socket, false);
|
|
54
|
+
socket.ping();
|
|
55
|
+
} else {
|
|
56
|
+
socket.terminate();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}, pingIntervalMs);
|
|
60
|
+
function watch(socket) {
|
|
61
|
+
watched.add(socket);
|
|
62
|
+
ponged.set(socket, true);
|
|
63
|
+
socket.on("pong", () => ponged.set(socket, true));
|
|
64
|
+
socket.once("close", () => watched.delete(socket));
|
|
65
|
+
socket.on("error", () => socket.terminate());
|
|
66
|
+
}
|
|
67
|
+
function connectControl() {
|
|
68
|
+
if (stopped)
|
|
69
|
+
return;
|
|
70
|
+
const socket = new WebSocket(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
|
|
71
|
+
control = socket;
|
|
72
|
+
watch(socket);
|
|
73
|
+
socket.on("message", (data, isBinary) => onControlMessage(socket, data, isBinary));
|
|
74
|
+
socket.once("close", onControlClosed);
|
|
75
|
+
}
|
|
76
|
+
function reconnect(at) {
|
|
77
|
+
if (stopped || retryTimer !== undefined)
|
|
78
|
+
return;
|
|
79
|
+
retryTimer = setTimeout(() => {
|
|
80
|
+
retryTimer = undefined;
|
|
81
|
+
connectControl();
|
|
82
|
+
}, at);
|
|
83
|
+
}
|
|
84
|
+
function onControlMessage(socket, data, isBinary) {
|
|
85
|
+
let frame;
|
|
86
|
+
try {
|
|
87
|
+
if (isBinary)
|
|
88
|
+
throw new Error("binary control frame");
|
|
89
|
+
frame = decodeRelayToHost(data.toString());
|
|
90
|
+
} catch {
|
|
91
|
+
socket.close();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
switch (frame.t) {
|
|
95
|
+
case "challenge": {
|
|
96
|
+
const noisePublicKey = toBase64Url(options.noisePublicKey);
|
|
97
|
+
const challenge = { relayOrigin: origin, hostId, nonce: frame.nonce };
|
|
98
|
+
socket.send(encodeRelayFrame({
|
|
99
|
+
t: "register",
|
|
100
|
+
hostId,
|
|
101
|
+
signature: signRelayChallenge(options.signingKey.secretKey, challenge),
|
|
102
|
+
signatureV2: signRelayChallengeV2(options.signingKey.secretKey, { ...challenge, noisePublicKey }),
|
|
103
|
+
noisePublicKey,
|
|
104
|
+
...options.accountOffer === undefined ? {} : { features: ["pairing"] }
|
|
105
|
+
}));
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "registered":
|
|
109
|
+
delay = minDelay;
|
|
110
|
+
setState({ status: "registered" });
|
|
111
|
+
break;
|
|
112
|
+
case "pairing-request": {
|
|
113
|
+
const accountOffer = options.accountOffer;
|
|
114
|
+
const refused = (reason) => {
|
|
115
|
+
socket.send(encodeRelayFrame({ t: "pairing-refused", requestId: frame.requestId, reason }));
|
|
116
|
+
};
|
|
117
|
+
if (accountOffer === undefined) {
|
|
118
|
+
refused("unavailable");
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
accountOffer().then((result) => {
|
|
122
|
+
if (socket !== control || socket.readyState !== WebSocket.OPEN)
|
|
123
|
+
return;
|
|
124
|
+
socket.send(encodeRelayFrame(result.ok ? { t: "pairing", requestId: frame.requestId, secret: result.secret, expiresAt: result.expiresAt } : { t: "pairing-refused", requestId: frame.requestId, reason: result.reason }));
|
|
125
|
+
}, () => {
|
|
126
|
+
if (socket === control && socket.readyState === WebSocket.OPEN)
|
|
127
|
+
refused("error");
|
|
128
|
+
});
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "incoming": {
|
|
132
|
+
if (pendingAccepts >= maxPendingAccepts)
|
|
133
|
+
break;
|
|
134
|
+
pendingAccepts += 1;
|
|
135
|
+
const data2 = new WebSocket(`${wsBase}/accept/${frame.connId}`, { handshakeTimeout: pingIntervalMs });
|
|
136
|
+
watch(data2);
|
|
137
|
+
const release = () => {
|
|
138
|
+
pendingAccepts -= 1;
|
|
139
|
+
clearTimeout(ageOut);
|
|
140
|
+
data2.off("close", release);
|
|
141
|
+
};
|
|
142
|
+
const ageOut = setTimeout(release, 1e4);
|
|
143
|
+
data2.once("close", release);
|
|
144
|
+
options.accept(data2);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function onControlClosed(code) {
|
|
150
|
+
control = undefined;
|
|
151
|
+
if (stopped)
|
|
152
|
+
return;
|
|
153
|
+
const rejection = REJECTION_REASONS[code];
|
|
154
|
+
if (rejection !== undefined) {
|
|
155
|
+
const reason = `relay ${origin}: ${rejection(hostId)} (${code})`;
|
|
156
|
+
setState({ status: "rejected", reason });
|
|
157
|
+
delay = maxDelay;
|
|
158
|
+
reconnect(maxDelay);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
setState({ status: "connecting" });
|
|
162
|
+
reconnect(delay);
|
|
163
|
+
delay = Math.min(maxDelay, delay * 2);
|
|
164
|
+
}
|
|
165
|
+
connectControl();
|
|
166
|
+
return {
|
|
167
|
+
hostId,
|
|
168
|
+
origin,
|
|
169
|
+
state: () => state,
|
|
170
|
+
close() {
|
|
171
|
+
stopped = true;
|
|
172
|
+
if (retryTimer !== undefined)
|
|
173
|
+
clearTimeout(retryTimer);
|
|
174
|
+
clearInterval(pingTimer);
|
|
175
|
+
for (const socket of watched)
|
|
176
|
+
socket.terminate();
|
|
177
|
+
return Promise.resolve();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export { relayWsBase, startRelayLink };
|
|
@@ -3,7 +3,8 @@ var HOST_SETTING_KEY_SET = {
|
|
|
3
3
|
defaultModel: true,
|
|
4
4
|
defaultThinkingLevel: true,
|
|
5
5
|
steeringMode: true,
|
|
6
|
-
followUpMode: true
|
|
6
|
+
followUpMode: true,
|
|
7
|
+
acceptAccountDevices: true
|
|
7
8
|
};
|
|
8
9
|
var HOST_SETTING_KEYS = Object.keys(HOST_SETTING_KEY_SET);
|
|
9
10
|
var THINKING_LEVEL_KEYS = { off: true, minimal: true, low: true, medium: true, high: true, xhigh: true, max: true };
|
|
@@ -21,7 +22,7 @@ var conversationStream = (id) => `conversation:${id}`;
|
|
|
21
22
|
var docStream = (kind, id) => `doc:${kind}:${id}`;
|
|
22
23
|
var UNAUTHORIZED_CLOSE_CODE = 4401;
|
|
23
24
|
var INVALID_FRAME_CLOSE_CODE = 4400;
|
|
24
|
-
var PROTOCOL = { major: 5, minor:
|
|
25
|
+
var PROTOCOL = { major: 5, minor: 8 };
|
|
25
26
|
var CALL_SINCE = {
|
|
26
27
|
logout: 1,
|
|
27
28
|
updateSettings: 2,
|
|
@@ -29,12 +30,13 @@ var CALL_SINCE = {
|
|
|
29
30
|
renameConversation: 4,
|
|
30
31
|
createAgent: 6,
|
|
31
32
|
updateAgent: 6,
|
|
32
|
-
deleteAgent: 6
|
|
33
|
+
deleteAgent: 6,
|
|
34
|
+
setPushToken: 7
|
|
33
35
|
};
|
|
34
36
|
function hostSupportsCall(hostMinor, method) {
|
|
35
37
|
return hostMinor >= (CALL_SINCE[method] ?? 0);
|
|
36
38
|
}
|
|
37
|
-
var FEATURE_SINCE = { images: 5 };
|
|
39
|
+
var FEATURE_SINCE = { images: 5, acceptAccountDevices: 8 };
|
|
38
40
|
function hostSupportsFeature(hostMinor, feature) {
|
|
39
41
|
return hostMinor >= FEATURE_SINCE[feature];
|
|
40
42
|
}
|
|
@@ -89,13 +91,15 @@ var CALL_METHODS = [
|
|
|
89
91
|
"updateSettings",
|
|
90
92
|
"createAgent",
|
|
91
93
|
"updateAgent",
|
|
92
|
-
"deleteAgent"
|
|
94
|
+
"deleteAgent",
|
|
95
|
+
"setPushToken"
|
|
93
96
|
];
|
|
94
97
|
var SETTING_VALIDATORS = {
|
|
95
98
|
defaultModel: (value) => value === null || record(value) && nonempty(value.provider) && nonempty(value.modelId),
|
|
96
99
|
defaultThinkingLevel: (value) => value === null || isThinkingLevel(value),
|
|
97
100
|
steeringMode: (value) => value === null || isQueueMode(value),
|
|
98
|
-
followUpMode: (value) => value === null || isQueueMode(value)
|
|
101
|
+
followUpMode: (value) => value === null || isQueueMode(value),
|
|
102
|
+
acceptAccountDevices: (value) => value === null || typeof value === "boolean"
|
|
99
103
|
};
|
|
100
104
|
function validCallArgs(method, args) {
|
|
101
105
|
const conversationId = () => typeof args.conversationId === "number" && Number.isSafeInteger(args.conversationId) && args.conversationId >= 1;
|
|
@@ -140,6 +144,8 @@ function validCallArgs(method, args) {
|
|
|
140
144
|
return role();
|
|
141
145
|
case "deleteAgent":
|
|
142
146
|
return typeof args.name === "string";
|
|
147
|
+
case "setPushToken":
|
|
148
|
+
return args.token === null || typeof args.token === "string";
|
|
143
149
|
case "updateSettings":
|
|
144
150
|
return record(args.patch) && Object.keys(args.patch).every((key) => HOST_SETTING_KEYS.includes(key)) && Object.entries(args.patch).every(([key, value]) => SETTING_VALIDATORS[key]?.(value) === true);
|
|
145
151
|
}
|
|
@@ -151,4 +157,97 @@ function nonempty(value) {
|
|
|
151
157
|
return typeof value === "string" && value.length > 0;
|
|
152
158
|
}
|
|
153
159
|
|
|
154
|
-
|
|
160
|
+
// packages/protocol/src/organization.ts
|
|
161
|
+
function organize(index, summaries) {
|
|
162
|
+
const summaryById = new Map(summaries.map((summary) => [summary.id, summary]));
|
|
163
|
+
const nodes = new Map;
|
|
164
|
+
const topLevel = new Map;
|
|
165
|
+
for (const entry of index.conversations) {
|
|
166
|
+
const home = entry.home;
|
|
167
|
+
if (home.kind === "project" && !index.projects.some((project) => project.path === home.path))
|
|
168
|
+
continue;
|
|
169
|
+
const summary = summaryById.get(entry.id);
|
|
170
|
+
if (summary === undefined)
|
|
171
|
+
continue;
|
|
172
|
+
topLevel.set(entry.id, entry);
|
|
173
|
+
nodes.set(entry.id, { summary, children: [] });
|
|
174
|
+
}
|
|
175
|
+
for (const summary of summaries) {
|
|
176
|
+
const chain = [summary];
|
|
177
|
+
let current = summary;
|
|
178
|
+
while (!topLevel.has(current.id)) {
|
|
179
|
+
const parent = current.parent === undefined ? undefined : summaryById.get(current.parent);
|
|
180
|
+
if (parent === undefined || chain.includes(parent))
|
|
181
|
+
break;
|
|
182
|
+
chain.push(parent);
|
|
183
|
+
current = parent;
|
|
184
|
+
}
|
|
185
|
+
if (!topLevel.has(current.id))
|
|
186
|
+
continue;
|
|
187
|
+
for (let i = chain.length - 1;i >= 0; i--) {
|
|
188
|
+
const member = chain[i];
|
|
189
|
+
if (!nodes.has(member.id))
|
|
190
|
+
nodes.set(member.id, { summary: member, children: [] });
|
|
191
|
+
if (i < chain.length - 1) {
|
|
192
|
+
const parent = nodes.get(chain[i + 1].id);
|
|
193
|
+
const child = nodes.get(member.id);
|
|
194
|
+
if (!parent.children.includes(child))
|
|
195
|
+
parent.children.push(child);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const byIdAscending = (a, b) => a.summary.id - b.summary.id;
|
|
200
|
+
const nodeList = (entry) => sortChildren(nodes.get(entry.id));
|
|
201
|
+
const sortChildren = (node) => ({
|
|
202
|
+
summary: node.summary,
|
|
203
|
+
children: [...node.children].sort(byIdAscending).map(sortChildren)
|
|
204
|
+
});
|
|
205
|
+
const sorted = [...topLevel.values()].sort((a, b) => b.createdAt - a.createdAt);
|
|
206
|
+
const entries = sorted.filter((entry) => entry.archived !== true);
|
|
207
|
+
return {
|
|
208
|
+
chats: entries.filter((entry) => entry.home.kind === "chat").map(nodeList),
|
|
209
|
+
projects: [...index.projects].sort((a, b) => b.addedAt - a.addedAt).map((project) => ({
|
|
210
|
+
project,
|
|
211
|
+
conversations: entries.filter((entry) => entry.home.kind === "project" && entry.home.path === project.path).map(nodeList)
|
|
212
|
+
})),
|
|
213
|
+
archived: sorted.filter((entry) => entry.archived === true).map((entry) => {
|
|
214
|
+
const home = entry.home;
|
|
215
|
+
const project = home.kind === "project" ? index.projects.find((each) => each.path === home.path) : undefined;
|
|
216
|
+
return { node: nodeList(entry), ...project === undefined ? {} : { project } };
|
|
217
|
+
})
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function checkoutOf(index, summaries, id) {
|
|
221
|
+
const record2 = index.checkouts?.find((checkout) => checkout.conversationId === id);
|
|
222
|
+
if (record2 !== undefined)
|
|
223
|
+
return record2;
|
|
224
|
+
const summaryById = new Map(summaries.map((summary) => [summary.id, summary]));
|
|
225
|
+
const seen = new Set;
|
|
226
|
+
let current = summaryById.get(id);
|
|
227
|
+
while (current !== undefined && !seen.has(current.id)) {
|
|
228
|
+
seen.add(current.id);
|
|
229
|
+
const ancestor = index.checkouts?.find((checkout) => checkout.conversationId === current.id);
|
|
230
|
+
if (ancestor !== undefined)
|
|
231
|
+
return ancestor;
|
|
232
|
+
current = current.parent === undefined ? undefined : summaryById.get(current.parent);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
function homeOf(index, summaries, id) {
|
|
237
|
+
const entry = index.conversations.find((candidate) => candidate.id === id);
|
|
238
|
+
if (entry !== undefined)
|
|
239
|
+
return entry.home;
|
|
240
|
+
const summaryById = new Map(summaries.map((summary) => [summary.id, summary]));
|
|
241
|
+
const seen = new Set;
|
|
242
|
+
let current = summaryById.get(id);
|
|
243
|
+
while (current !== undefined && !seen.has(current.id)) {
|
|
244
|
+
seen.add(current.id);
|
|
245
|
+
const ancestor = index.conversations.find((candidate) => candidate.id === current.id);
|
|
246
|
+
if (ancestor !== undefined)
|
|
247
|
+
return ancestor.home;
|
|
248
|
+
current = current.parent === undefined ? undefined : summaryById.get(current.parent);
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export { HOST_SETTING_KEYS, THINKING_LEVELS, isThinkingLevel, isQueueMode, HOST_STREAM_NAMES, hostStreams, conversationStream, docStream, UNAUTHORIZED_CLOSE_CODE, INVALID_FRAME_CLOSE_CODE, PROTOCOL, hostSupportsCall, hostSupportsFeature, parseClientFrame, organize, checkoutOf, homeOf };
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
main,
|
|
4
4
|
restartService
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import"./chunk-
|
|
7
|
-
import"./chunk-
|
|
8
|
-
import"./chunk-
|
|
5
|
+
} from "./chunk-akgxwg8f.js";
|
|
6
|
+
import"./chunk-fgx2sh7d.js";
|
|
7
|
+
import"./chunk-374zqx5h.js";
|
|
8
|
+
import"./chunk-myjp910s.js";
|
|
9
9
|
import"./chunk-cjfmfyjb.js";
|
|
10
10
|
import"./chunk-df32avw6.js";
|
|
11
11
|
export {
|