pinomad 0.4.0 → 0.5.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/README.md +4 -2
- package/dist/{chunk-fgx2sh7d.js → chunk-1d9sppj0.js} +1 -1
- package/dist/{chunk-5785kwej.js → chunk-26d04z02.js} +3 -5
- package/dist/{chunk-vw5yb5pd.js → chunk-4by4x5c4.js} +290 -37
- package/dist/{chunk-prhvksh3.js → chunk-5ng86fmy.js} +4 -4
- package/dist/{chunk-b3f0ced1.js → chunk-9d59jjmt.js} +11 -14
- package/dist/{chunk-akgxwg8f.js → chunk-aqj4wg33.js} +2 -2
- package/dist/{chunk-17p6bfvy.js → chunk-crv3dqzp.js} +4 -4
- package/dist/{chunk-z5pmcscx.js → chunk-d4wdt5qs.js} +594 -212
- package/dist/{chunk-837gh8nr.js → chunk-ebfaf3k2.js} +69 -17
- package/dist/{chunk-xhwkx9bd.js → chunk-f0v3sht0.js} +11 -14
- package/dist/{chunk-n73t5xzw.js → chunk-gqr7ahv4.js} +1 -1
- package/dist/{chunk-myjp910s.js → chunk-hsp12cjt.js} +10 -6
- package/dist/{chunk-7gmkgyaz.js → chunk-kmbh44dt.js} +286 -2
- package/dist/{chunk-374zqx5h.js → chunk-m4q9wafw.js} +14 -1
- package/dist/{chunk-d4y99pfy.js → chunk-zttf1hvh.js} +2 -2
- package/dist/pinomad.js +14 -14
- package/package.json +1 -1
- package/web/apple-touch-icon.png +0 -0
- package/web/assets/index-BkF0-im5.css +1 -0
- package/web/assets/{index-8mNNPXPT.js → index-D4lgm3Y4.js} +89 -89
- package/web/favicon.svg +1 -0
- package/web/index.html +4 -2
- package/dist/chunk-302y6by9.js +0 -50
- package/dist/chunk-knaw1a9g.js +0 -182
- package/dist/chunk-xrvmnymc.js +0 -33
- package/web/assets/index-BFJJgGGo.css +0 -1
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
generateKeyPair,
|
|
4
4
|
keyPairFromPrivate,
|
|
5
5
|
toBase64Url
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-kmbh44dt.js";
|
|
7
7
|
|
|
8
8
|
// apps/host/src/devices.ts
|
|
9
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import { link, readFile, unlink, writeFile } from "node:fs/promises";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
12
12
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
@@ -62,14 +62,8 @@ async function loadHostKey(dataDir) {
|
|
|
62
62
|
if (error.code !== "ENOENT")
|
|
63
63
|
throw error;
|
|
64
64
|
const pair = generateKeyPair();
|
|
65
|
-
|
|
66
|
-
await writeFile(path, `${toBase64Url(pair.privateKey)}
|
|
67
|
-
`, { flag: "wx", mode: 384 });
|
|
68
|
-
} catch (race) {
|
|
69
|
-
if (race.code !== "EEXIST")
|
|
70
|
-
throw race;
|
|
65
|
+
if (!await createKeyFile(path, toBase64Url(pair.privateKey)))
|
|
71
66
|
return loadHostKey(dataDir);
|
|
72
|
-
}
|
|
73
67
|
return pair;
|
|
74
68
|
}
|
|
75
69
|
}
|
|
@@ -84,17 +78,26 @@ async function loadRelayKey(dataDir) {
|
|
|
84
78
|
if (error.code !== "ENOENT")
|
|
85
79
|
throw error;
|
|
86
80
|
const { secretKey, publicKey } = ed25519.keygen();
|
|
87
|
-
|
|
88
|
-
await writeFile(path, `${toBase64Url(secretKey)}
|
|
89
|
-
`, { flag: "wx", mode: 384 });
|
|
90
|
-
} catch (race) {
|
|
91
|
-
if (race.code !== "EEXIST")
|
|
92
|
-
throw race;
|
|
81
|
+
if (!await createKeyFile(path, toBase64Url(secretKey)))
|
|
93
82
|
return loadRelayKey(dataDir);
|
|
94
|
-
}
|
|
95
83
|
return { secretKey, publicKey };
|
|
96
84
|
}
|
|
97
85
|
}
|
|
86
|
+
async function createKeyFile(path, key) {
|
|
87
|
+
const tmp = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
88
|
+
try {
|
|
89
|
+
await writeFile(tmp, `${key}
|
|
90
|
+
`, { flag: "wx", mode: 384 });
|
|
91
|
+
await link(tmp, path);
|
|
92
|
+
return true;
|
|
93
|
+
} catch (race) {
|
|
94
|
+
if (race.code !== "EEXIST")
|
|
95
|
+
throw race;
|
|
96
|
+
return false;
|
|
97
|
+
} finally {
|
|
98
|
+
await unlink(tmp).catch(() => {});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
98
101
|
function pairingOffers(ttlMs = 5 * 60 * 1000) {
|
|
99
102
|
const live = new Map;
|
|
100
103
|
const prune = () => {
|
|
@@ -135,4 +138,53 @@ function pairingOffers(ttlMs = 5 * 60 * 1000) {
|
|
|
135
138
|
};
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
|
|
141
|
+
// apps/host/src/server-config.ts
|
|
142
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
143
|
+
import { readFile as readFile2, rename, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
144
|
+
import { join as join2 } from "node:path";
|
|
145
|
+
var SERVER_CONFIG_FILE = "server.json";
|
|
146
|
+
var normalizeOrigin = (origin) => origin.replace(/\/+$/, "");
|
|
147
|
+
async function readServerConfig(dataDir) {
|
|
148
|
+
let text;
|
|
149
|
+
try {
|
|
150
|
+
text = await readFile2(join2(dataDir, SERVER_CONFIG_FILE), "utf8");
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error.code === "ENOENT")
|
|
153
|
+
return;
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
let value;
|
|
157
|
+
try {
|
|
158
|
+
value = JSON.parse(text);
|
|
159
|
+
} catch {
|
|
160
|
+
throw new Error(`${join2(dataDir, SERVER_CONFIG_FILE)} is not valid JSON — delete it or run \`pinomad leave\``);
|
|
161
|
+
}
|
|
162
|
+
const origin = value?.origin;
|
|
163
|
+
if (typeof origin !== "string" || origin.trim() === "") {
|
|
164
|
+
throw new Error(`${join2(dataDir, SERVER_CONFIG_FILE)} is missing a string "origin"`);
|
|
165
|
+
}
|
|
166
|
+
const binding = value.binding;
|
|
167
|
+
return {
|
|
168
|
+
origin,
|
|
169
|
+
...typeof binding === "string" && binding !== "" ? { binding } : {}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async function writeServerConfig(dataDir, config) {
|
|
173
|
+
const path = join2(dataDir, SERVER_CONFIG_FILE);
|
|
174
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
175
|
+
await writeFile2(tmp, `${JSON.stringify({ origin: normalizeOrigin(config.origin), binding: randomBytes2(16).toString("base64url") })}
|
|
176
|
+
`, { mode: 384 });
|
|
177
|
+
await rename(tmp, path);
|
|
178
|
+
}
|
|
179
|
+
var removeServerConfig = (dataDir) => rm(join2(dataDir, SERVER_CONFIG_FILE), { force: true });
|
|
180
|
+
async function readServerBinding(dataDir) {
|
|
181
|
+
return (await readServerConfig(dataDir))?.binding;
|
|
182
|
+
}
|
|
183
|
+
function resolveRelayOrigin(flag, config) {
|
|
184
|
+
if (flag !== undefined && config !== undefined && normalizeOrigin(flag) !== normalizeOrigin(config.origin)) {
|
|
185
|
+
throw new Error(`--relay ${flag} disagrees with ${SERVER_CONFIG_FILE} (${config.origin}) — remove one`);
|
|
186
|
+
}
|
|
187
|
+
return flag ?? config?.origin;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { DevicesDoc, ensureDevices, RevocationsDoc, admitDevice, loadHostKey, loadRelayKey, pairingOffers, normalizeOrigin, readServerConfig, writeServerConfig, removeServerConfig, readServerBinding, resolveRelayOrigin };
|
|
@@ -3,28 +3,25 @@ import {
|
|
|
3
3
|
assertSecureServerOrigin,
|
|
4
4
|
probeServerAccounts,
|
|
5
5
|
runServerOperation
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-26d04z02.js";
|
|
7
7
|
import {
|
|
8
|
+
loadHostKey,
|
|
9
|
+
loadRelayKey,
|
|
8
10
|
readServerConfig,
|
|
9
11
|
removeServerConfig
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import
|
|
12
|
+
} from "./chunk-ebfaf3k2.js";
|
|
13
|
+
import {
|
|
14
|
+
relayHostId
|
|
15
|
+
} from "./chunk-kmbh44dt.js";
|
|
12
16
|
import {
|
|
13
17
|
argValue,
|
|
14
18
|
restartService,
|
|
15
19
|
serviceManager,
|
|
16
20
|
withoutRelayArgs
|
|
17
|
-
} from "./chunk-
|
|
18
|
-
import"./chunk-
|
|
19
|
-
import"./chunk-
|
|
20
|
-
import"./chunk-
|
|
21
|
-
import {
|
|
22
|
-
loadHostKey,
|
|
23
|
-
loadRelayKey
|
|
24
|
-
} from "./chunk-837gh8nr.js";
|
|
25
|
-
import {
|
|
26
|
-
relayHostId
|
|
27
|
-
} from "./chunk-7gmkgyaz.js";
|
|
21
|
+
} from "./chunk-aqj4wg33.js";
|
|
22
|
+
import"./chunk-1d9sppj0.js";
|
|
23
|
+
import"./chunk-m4q9wafw.js";
|
|
24
|
+
import"./chunk-hsp12cjt.js";
|
|
28
25
|
import {
|
|
29
26
|
DEFAULT_DATA_DIR
|
|
30
27
|
} from "./chunk-cjfmfyjb.js";
|
|
@@ -22,7 +22,7 @@ var conversationStream = (id) => `conversation:${id}`;
|
|
|
22
22
|
var docStream = (kind, id) => `doc:${kind}:${id}`;
|
|
23
23
|
var UNAUTHORIZED_CLOSE_CODE = 4401;
|
|
24
24
|
var INVALID_FRAME_CLOSE_CODE = 4400;
|
|
25
|
-
var PROTOCOL = { major: 5, minor:
|
|
25
|
+
var PROTOCOL = { major: 5, minor: 10 };
|
|
26
26
|
var CALL_SINCE = {
|
|
27
27
|
logout: 1,
|
|
28
28
|
updateSettings: 2,
|
|
@@ -31,12 +31,13 @@ var CALL_SINCE = {
|
|
|
31
31
|
createAgent: 6,
|
|
32
32
|
updateAgent: 6,
|
|
33
33
|
deleteAgent: 6,
|
|
34
|
-
setPushToken: 7
|
|
34
|
+
setPushToken: 7,
|
|
35
|
+
listFiles: 10
|
|
35
36
|
};
|
|
36
37
|
function hostSupportsCall(hostMinor, method) {
|
|
37
38
|
return hostMinor >= (CALL_SINCE[method] ?? 0);
|
|
38
39
|
}
|
|
39
|
-
var FEATURE_SINCE = { images: 5, acceptAccountDevices: 8 };
|
|
40
|
+
var FEATURE_SINCE = { images: 5, acceptAccountDevices: 8, apnsPush: 9, agentConversations: 10 };
|
|
40
41
|
function hostSupportsFeature(hostMinor, feature) {
|
|
41
42
|
return hostMinor >= FEATURE_SINCE[feature];
|
|
42
43
|
}
|
|
@@ -92,7 +93,8 @@ var CALL_METHODS = [
|
|
|
92
93
|
"createAgent",
|
|
93
94
|
"updateAgent",
|
|
94
95
|
"deleteAgent",
|
|
95
|
-
"setPushToken"
|
|
96
|
+
"setPushToken",
|
|
97
|
+
"listFiles"
|
|
96
98
|
];
|
|
97
99
|
var SETTING_VALIDATORS = {
|
|
98
100
|
defaultModel: (value) => value === null || record(value) && nonempty(value.provider) && nonempty(value.modelId),
|
|
@@ -128,7 +130,9 @@ function validCallArgs(method, args) {
|
|
|
128
130
|
case "listFolders":
|
|
129
131
|
return args.path === undefined || nonempty(args.path);
|
|
130
132
|
case "createConversation":
|
|
131
|
-
return record(args.home) && (args.home.kind === "chat" || args.home.kind === "project" && nonempty(args.home.path)) && typeof args.text === "string" && (nonempty(args.text) || Array.isArray(args.images) && args.images.length > 0) && nonempty(args.requestId) && images() && (args.checkout === undefined || args.checkout === "worktree" || args.checkout === "project") && (args.model === undefined || record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId)) && (args.thinkingLevel === undefined || nonempty(args.thinkingLevel));
|
|
133
|
+
return record(args.home) && (args.home.kind === "chat" || args.home.kind === "project" && nonempty(args.home.path)) && typeof args.text === "string" && (nonempty(args.text) || Array.isArray(args.images) && args.images.length > 0) && nonempty(args.requestId) && images() && (args.checkout === undefined || args.checkout === "worktree" || args.checkout === "project") && (args.model === undefined || record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId)) && (args.thinkingLevel === undefined || nonempty(args.thinkingLevel)) && (args.agent === undefined || nonempty(args.agent));
|
|
134
|
+
case "listFiles":
|
|
135
|
+
return typeof args.query === "string" && record(args.scope) && (args.scope.kind === "conversation" && typeof args.scope.conversationId === "number" && Number.isSafeInteger(args.scope.conversationId) && args.scope.conversationId >= 1 || args.scope.kind === "project" && nonempty(args.scope.path) && (args.scope.checkout === "worktree" || args.scope.checkout === "project"));
|
|
132
136
|
case "archive":
|
|
133
137
|
return conversationId() && typeof args.archived === "boolean";
|
|
134
138
|
case "renameConversation":
|
|
@@ -145,7 +149,7 @@ function validCallArgs(method, args) {
|
|
|
145
149
|
case "deleteAgent":
|
|
146
150
|
return typeof args.name === "string";
|
|
147
151
|
case "setPushToken":
|
|
148
|
-
return args.token === null || typeof args.token === "string";
|
|
152
|
+
return args.token === null || typeof args.token === "string" || record(args.token) && typeof args.token.apns === "string" && (args.token.environment === "production" || args.token.environment === "development");
|
|
149
153
|
case "updateSettings":
|
|
150
154
|
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);
|
|
151
155
|
}
|
|
@@ -290,6 +290,7 @@ function encodeRelayFrame(frame) {
|
|
|
290
290
|
return JSON.stringify(frame);
|
|
291
291
|
}
|
|
292
292
|
var isString2 = (value) => typeof value === "string";
|
|
293
|
+
var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
293
294
|
var isBoundedString = (value, max = 64) => isString2(value) && value.length > 0 && value.length <= max;
|
|
294
295
|
function decodeJson2(text) {
|
|
295
296
|
try {
|
|
@@ -306,7 +307,7 @@ function decodeRelayToHost(text) {
|
|
|
306
307
|
throw new Error("malformed challenge");
|
|
307
308
|
return { t: "challenge", nonce: value.nonce };
|
|
308
309
|
case "registered":
|
|
309
|
-
return
|
|
310
|
+
return decodeRegistered(value);
|
|
310
311
|
case "incoming":
|
|
311
312
|
if (!isString2(value.connId))
|
|
312
313
|
throw new Error("malformed incoming");
|
|
@@ -315,6 +316,8 @@ function decodeRelayToHost(text) {
|
|
|
315
316
|
if (!isBoundedString(value.requestId))
|
|
316
317
|
throw new Error("malformed pairing-request");
|
|
317
318
|
return { t: "pairing-request", requestId: value.requestId };
|
|
319
|
+
case "push-result":
|
|
320
|
+
return decodePushResult(value);
|
|
318
321
|
case "joined":
|
|
319
322
|
if (!isString2(value.username))
|
|
320
323
|
throw new Error("malformed joined");
|
|
@@ -329,11 +332,52 @@ function decodeRelayToHost(text) {
|
|
|
329
332
|
throw new Error("unknown relay frame");
|
|
330
333
|
}
|
|
331
334
|
}
|
|
335
|
+
function decodeRegistered(value) {
|
|
336
|
+
if (value.push === undefined)
|
|
337
|
+
return { t: "registered" };
|
|
338
|
+
if (!Array.isArray(value.push) || value.push.length > 8 || !value.push.every(isString2)) {
|
|
339
|
+
throw new Error("malformed registered");
|
|
340
|
+
}
|
|
341
|
+
const push = value.push.filter((entry) => entry === "production" || entry === "development");
|
|
342
|
+
return { t: "registered", push };
|
|
343
|
+
}
|
|
344
|
+
function decodePushResult(value) {
|
|
345
|
+
if (!isBoundedString(value.requestId) || !isBoundedString(value.reason, 256) || value.result !== "accepted" && value.result !== "unregistered" && value.result !== "retry" && value.result !== "rejected" && value.result !== "unavailable") {
|
|
346
|
+
throw new Error("malformed push-result");
|
|
347
|
+
}
|
|
348
|
+
if (value.result === "unregistered") {
|
|
349
|
+
if (typeof value.unregisteredAt !== "number" || !Number.isFinite(value.unregisteredAt)) {
|
|
350
|
+
throw new Error("malformed push-result");
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
t: "push-result",
|
|
354
|
+
requestId: value.requestId,
|
|
355
|
+
result: "unregistered",
|
|
356
|
+
reason: value.reason,
|
|
357
|
+
unregisteredAt: value.unregisteredAt
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (value.result === "retry") {
|
|
361
|
+
if (value.retryAfterMs !== undefined && (typeof value.retryAfterMs !== "number" || !Number.isFinite(value.retryAfterMs))) {
|
|
362
|
+
throw new Error("malformed push-result");
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
t: "push-result",
|
|
366
|
+
requestId: value.requestId,
|
|
367
|
+
result: "retry",
|
|
368
|
+
reason: value.reason,
|
|
369
|
+
...value.retryAfterMs === undefined ? {} : { retryAfterMs: value.retryAfterMs }
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return { t: "push-result", requestId: value.requestId, result: value.result, reason: value.reason };
|
|
373
|
+
}
|
|
332
374
|
function decodeHostToRelay(text) {
|
|
333
375
|
const value = decodeJson2(text);
|
|
334
376
|
switch (value?.t) {
|
|
335
377
|
case "register":
|
|
336
378
|
return decodeRegister(value);
|
|
379
|
+
case "push":
|
|
380
|
+
return decodePush(value);
|
|
337
381
|
case "pairing":
|
|
338
382
|
if (!isBoundedString(value.requestId) || !isBoundedString(value.secret) || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt)) {
|
|
339
383
|
throw new Error("malformed pairing");
|
|
@@ -348,6 +392,25 @@ function decodeHostToRelay(text) {
|
|
|
348
392
|
throw new Error("unknown host frame");
|
|
349
393
|
}
|
|
350
394
|
}
|
|
395
|
+
function decodePush(value) {
|
|
396
|
+
const n = value.notification;
|
|
397
|
+
if (!isBoundedString(value.requestId) || !isBoundedString(value.token, 256) || value.environment !== "production" && value.environment !== "development" || !isRecord(n) || !isBoundedString(n.title, 512) || !isBoundedString(n.body, 256) || !isBoundedString(n.collapseId, 64) || !isBoundedString(n.hostKey, 64) || typeof n.conversationId !== "number" || !Number.isSafeInteger(n.conversationId) || n.conversationId < 1) {
|
|
398
|
+
throw new Error("malformed push");
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
t: "push",
|
|
402
|
+
requestId: value.requestId,
|
|
403
|
+
token: value.token,
|
|
404
|
+
environment: value.environment,
|
|
405
|
+
notification: {
|
|
406
|
+
title: n.title,
|
|
407
|
+
body: n.body,
|
|
408
|
+
collapseId: n.collapseId,
|
|
409
|
+
hostKey: n.hostKey,
|
|
410
|
+
conversationId: n.conversationId
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
351
414
|
function decodeRegister(value) {
|
|
352
415
|
if (value.t !== "register" || !isString2(value.hostId) || !isString2(value.signature)) {
|
|
353
416
|
throw new Error("malformed register");
|
|
@@ -417,4 +480,225 @@ function verifyRelayChallengeV2(signature, args) {
|
|
|
417
480
|
}
|
|
418
481
|
}
|
|
419
482
|
|
|
420
|
-
|
|
483
|
+
// apps/host/src/relay-link.ts
|
|
484
|
+
import { randomBytes } from "node:crypto";
|
|
485
|
+
import { WebSocket as WebSocket2 } from "ws";
|
|
486
|
+
function relayWsBase(origin) {
|
|
487
|
+
const normalized = origin.replace(/\/+$/, "");
|
|
488
|
+
if (normalized.startsWith("https://"))
|
|
489
|
+
return `wss://${normalized.slice("https://".length)}`;
|
|
490
|
+
if (normalized.startsWith("http://"))
|
|
491
|
+
return `ws://${normalized.slice("http://".length)}`;
|
|
492
|
+
throw new Error(`relay origin must be an http(s) URL: ${origin}`);
|
|
493
|
+
}
|
|
494
|
+
var REJECTION_REASONS = {
|
|
495
|
+
[RELAY_CLOSE.badSignature]: () => `the relay rejected this host's registration signature — a version mismatch?`,
|
|
496
|
+
[RELAY_CLOSE.hostNotAllowed]: (hostId) => `the server does not allow this host (${hostId}); join it to an account with \`pinomad join\``,
|
|
497
|
+
[RELAY_CLOSE.replaced]: () => `the relay replaced this control connection — another process registered the same hostId`
|
|
498
|
+
};
|
|
499
|
+
var PUSH_TIMEOUT_MS = 1e4;
|
|
500
|
+
function startRelayLink(options) {
|
|
501
|
+
const origin = options.origin.replace(/\/+$/, "");
|
|
502
|
+
const wsBase = relayWsBase(origin);
|
|
503
|
+
const hostId = relayHostId(options.signingKey.publicKey);
|
|
504
|
+
const minDelay = options.reconnectDelayMs?.min ?? 1000;
|
|
505
|
+
const maxDelay = options.reconnectDelayMs?.max ?? 30000;
|
|
506
|
+
const maxPendingAccepts = options.maxPendingAccepts ?? 16;
|
|
507
|
+
let state = { status: "connecting" };
|
|
508
|
+
let stopped = false;
|
|
509
|
+
let control;
|
|
510
|
+
let delay = minDelay;
|
|
511
|
+
let retryTimer;
|
|
512
|
+
let pendingAccepts = 0;
|
|
513
|
+
let pushEnvironments;
|
|
514
|
+
const pushes = new Map;
|
|
515
|
+
function setState(next) {
|
|
516
|
+
const changed = state.status !== next.status || state.status === "rejected" && next.status === "rejected" && state.reason !== next.reason;
|
|
517
|
+
state = next;
|
|
518
|
+
if (changed)
|
|
519
|
+
options.onStateChange?.(next);
|
|
520
|
+
}
|
|
521
|
+
const watched = new Set;
|
|
522
|
+
const ponged = new WeakMap;
|
|
523
|
+
const pingIntervalMs = options.pingIntervalMs ?? 30000;
|
|
524
|
+
const pingTimer = setInterval(() => {
|
|
525
|
+
for (const socket of watched) {
|
|
526
|
+
if (socket.readyState !== WebSocket2.OPEN)
|
|
527
|
+
continue;
|
|
528
|
+
if (ponged.get(socket) === true) {
|
|
529
|
+
ponged.set(socket, false);
|
|
530
|
+
socket.ping();
|
|
531
|
+
} else {
|
|
532
|
+
socket.terminate();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}, pingIntervalMs);
|
|
536
|
+
function watch(socket) {
|
|
537
|
+
watched.add(socket);
|
|
538
|
+
ponged.set(socket, true);
|
|
539
|
+
socket.on("pong", () => ponged.set(socket, true));
|
|
540
|
+
socket.once("close", () => watched.delete(socket));
|
|
541
|
+
socket.on("error", () => socket.terminate());
|
|
542
|
+
}
|
|
543
|
+
function connectControl() {
|
|
544
|
+
if (stopped)
|
|
545
|
+
return;
|
|
546
|
+
const socket = new WebSocket2(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
|
|
547
|
+
control = socket;
|
|
548
|
+
pushEnvironments = undefined;
|
|
549
|
+
watch(socket);
|
|
550
|
+
socket.on("message", (data, isBinary) => onControlMessage(socket, data, isBinary));
|
|
551
|
+
socket.once("close", (code) => onControlClosed(socket, code));
|
|
552
|
+
}
|
|
553
|
+
function reconnect(at) {
|
|
554
|
+
if (stopped || retryTimer !== undefined)
|
|
555
|
+
return;
|
|
556
|
+
retryTimer = setTimeout(() => {
|
|
557
|
+
retryTimer = undefined;
|
|
558
|
+
connectControl();
|
|
559
|
+
}, at);
|
|
560
|
+
}
|
|
561
|
+
function onControlMessage(socket, data, isBinary) {
|
|
562
|
+
let frame;
|
|
563
|
+
try {
|
|
564
|
+
if (isBinary)
|
|
565
|
+
throw new Error("binary control frame");
|
|
566
|
+
frame = decodeRelayToHost(data.toString());
|
|
567
|
+
} catch {
|
|
568
|
+
socket.close();
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
switch (frame.t) {
|
|
572
|
+
case "challenge": {
|
|
573
|
+
const noisePublicKey = toBase64Url(options.noisePublicKey);
|
|
574
|
+
const challenge = { relayOrigin: origin, hostId, nonce: frame.nonce };
|
|
575
|
+
socket.send(encodeRelayFrame({
|
|
576
|
+
t: "register",
|
|
577
|
+
hostId,
|
|
578
|
+
signature: signRelayChallenge(options.signingKey.secretKey, challenge),
|
|
579
|
+
signatureV2: signRelayChallengeV2(options.signingKey.secretKey, { ...challenge, noisePublicKey }),
|
|
580
|
+
noisePublicKey,
|
|
581
|
+
...options.accountOffer === undefined ? {} : { features: ["pairing"] }
|
|
582
|
+
}));
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case "registered":
|
|
586
|
+
delay = minDelay;
|
|
587
|
+
pushEnvironments = frame.push;
|
|
588
|
+
setState({ status: "registered", ...frame.push === undefined ? {} : { push: frame.push } });
|
|
589
|
+
break;
|
|
590
|
+
case "push-result": {
|
|
591
|
+
const request = pushes.get(frame.requestId);
|
|
592
|
+
if (request === undefined || request.socket !== socket)
|
|
593
|
+
break;
|
|
594
|
+
pushes.delete(frame.requestId);
|
|
595
|
+
const { t: _t, requestId: _id, ...verdict } = frame;
|
|
596
|
+
request.settle(verdict);
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
case "pairing-request": {
|
|
600
|
+
const accountOffer = options.accountOffer;
|
|
601
|
+
const refused = (reason) => {
|
|
602
|
+
socket.send(encodeRelayFrame({ t: "pairing-refused", requestId: frame.requestId, reason }));
|
|
603
|
+
};
|
|
604
|
+
if (accountOffer === undefined) {
|
|
605
|
+
refused("unavailable");
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
accountOffer().then((result) => {
|
|
609
|
+
if (socket !== control || socket.readyState !== WebSocket2.OPEN)
|
|
610
|
+
return;
|
|
611
|
+
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 }));
|
|
612
|
+
}, () => {
|
|
613
|
+
if (socket === control && socket.readyState === WebSocket2.OPEN)
|
|
614
|
+
refused("error");
|
|
615
|
+
});
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
case "incoming": {
|
|
619
|
+
if (pendingAccepts >= maxPendingAccepts)
|
|
620
|
+
break;
|
|
621
|
+
pendingAccepts += 1;
|
|
622
|
+
const data2 = new WebSocket2(`${wsBase}/accept/${frame.connId}`, { handshakeTimeout: pingIntervalMs });
|
|
623
|
+
watch(data2);
|
|
624
|
+
const release = () => {
|
|
625
|
+
pendingAccepts -= 1;
|
|
626
|
+
clearTimeout(ageOut);
|
|
627
|
+
data2.off("close", release);
|
|
628
|
+
};
|
|
629
|
+
const ageOut = setTimeout(release, 1e4);
|
|
630
|
+
data2.once("close", release);
|
|
631
|
+
options.accept(data2);
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function onControlClosed(socket, code) {
|
|
637
|
+
control = undefined;
|
|
638
|
+
pushEnvironments = undefined;
|
|
639
|
+
for (const [requestId, request] of pushes) {
|
|
640
|
+
if (request.socket === socket) {
|
|
641
|
+
pushes.delete(requestId);
|
|
642
|
+
request.settle({ result: "retry", reason: "control closed" });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (stopped)
|
|
646
|
+
return;
|
|
647
|
+
const rejection = REJECTION_REASONS[code];
|
|
648
|
+
if (rejection !== undefined) {
|
|
649
|
+
const reason = `relay ${origin}: ${rejection(hostId)} (${code})`;
|
|
650
|
+
setState({ status: "rejected", reason });
|
|
651
|
+
delay = maxDelay;
|
|
652
|
+
reconnect(maxDelay);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
setState({ status: "connecting" });
|
|
656
|
+
reconnect(delay);
|
|
657
|
+
delay = Math.min(maxDelay, delay * 2);
|
|
658
|
+
}
|
|
659
|
+
connectControl();
|
|
660
|
+
return {
|
|
661
|
+
hostId,
|
|
662
|
+
origin,
|
|
663
|
+
state: () => state,
|
|
664
|
+
sendPush(request) {
|
|
665
|
+
const socket = control;
|
|
666
|
+
if (stopped || socket === undefined || socket.readyState !== WebSocket2.OPEN || state.status !== "registered") {
|
|
667
|
+
return Promise.resolve({ result: "retry", reason: "control disconnected" });
|
|
668
|
+
}
|
|
669
|
+
const envs = pushEnvironments;
|
|
670
|
+
if (envs === undefined) {
|
|
671
|
+
return Promise.resolve({ result: "unavailable", reason: "server does not declare push" });
|
|
672
|
+
}
|
|
673
|
+
if (!envs.includes(request.environment)) {
|
|
674
|
+
return Promise.resolve({ result: "unavailable", reason: `environment ${request.environment} not declared` });
|
|
675
|
+
}
|
|
676
|
+
return new Promise((resolve) => {
|
|
677
|
+
const requestId = randomBytes(16).toString("base64url");
|
|
678
|
+
const timer = setTimeout(() => {
|
|
679
|
+
if (pushes.delete(requestId))
|
|
680
|
+
resolve({ result: "retry", reason: "timeout" });
|
|
681
|
+
}, PUSH_TIMEOUT_MS);
|
|
682
|
+
pushes.set(requestId, {
|
|
683
|
+
socket,
|
|
684
|
+
settle: (verdict) => {
|
|
685
|
+
clearTimeout(timer);
|
|
686
|
+
resolve(verdict);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
socket.send(encodeRelayFrame({ t: "push", requestId, ...request }));
|
|
690
|
+
});
|
|
691
|
+
},
|
|
692
|
+
close() {
|
|
693
|
+
stopped = true;
|
|
694
|
+
if (retryTimer !== undefined)
|
|
695
|
+
clearTimeout(retryTimer);
|
|
696
|
+
clearInterval(pingTimer);
|
|
697
|
+
for (const socket of watched)
|
|
698
|
+
socket.terminate();
|
|
699
|
+
return Promise.resolve();
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export { generateKeyPair, keyPairFromPrivate, respondIK, SECURE_PROLOGUE, encodeHostHello, decodeClientHello, toBase64Url, fromBase64Url, SecureSession, RELAY_CLOSE, encodeRelayFrame, decodeRelayToHost, decodeHostToRelay, relayHostId, signRelayChallenge, verifyRelayChallenge, signRelayChallengeV2, verifyRelayChallengeV2, relayWsBase, startRelayLink };
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
hostSupportsCall,
|
|
12
12
|
hostSupportsFeature,
|
|
13
13
|
organize
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-hsp12cjt.js";
|
|
15
15
|
|
|
16
16
|
// packages/protocol/src/remote-durable.ts
|
|
17
17
|
import { applyImmutable } from "@earendil-works/chord/delta";
|
|
@@ -440,6 +440,9 @@ class RemoteClient {
|
|
|
440
440
|
if (images.length > 0 && !hostSupportsFeature(this.#hostMinor, "images")) {
|
|
441
441
|
throw new Error("The host is too old for images (needs protocol 5.5); run `pinomad upgrade` on the host");
|
|
442
442
|
}
|
|
443
|
+
if (options?.agent !== undefined && !hostSupportsFeature(this.#hostMinor, "agentConversations")) {
|
|
444
|
+
throw new Error("The host is too old for named agents (needs protocol 5.10); run `pinomad upgrade` on the host");
|
|
445
|
+
}
|
|
443
446
|
const { conversationId: conversationId2 } = await this.#call("createConversation", {
|
|
444
447
|
home,
|
|
445
448
|
text,
|
|
@@ -447,6 +450,7 @@ class RemoteClient {
|
|
|
447
450
|
...options?.checkout === undefined ? {} : { checkout: options.checkout },
|
|
448
451
|
...options?.model === undefined ? {} : { model: options.model },
|
|
449
452
|
...options?.thinkingLevel === undefined ? {} : { thinkingLevel: options.thinkingLevel },
|
|
453
|
+
...options?.agent === undefined ? {} : { agent: options.agent },
|
|
450
454
|
...images.length === 0 ? {} : { images }
|
|
451
455
|
});
|
|
452
456
|
await this.#switch(conversationId2);
|
|
@@ -510,6 +514,12 @@ class RemoteClient {
|
|
|
510
514
|
}),
|
|
511
515
|
answer: (kind, requestId, answers, target) => this.#command(() => this.#call("answer", { conversationId: target ?? conversationId(), kind, requestId, answers })),
|
|
512
516
|
changes: () => this.#request(() => this.#call("changes", { conversationId: conversationId() })),
|
|
517
|
+
listFiles: (scope, query) => {
|
|
518
|
+
if (!hostSupportsCall(this.#hostMinor, "listFiles")) {
|
|
519
|
+
return Promise.reject(new Error("The host is too old for this (needs protocol 5.10); run `pinomad upgrade` on the host"));
|
|
520
|
+
}
|
|
521
|
+
return this.#call("listFiles", { scope, query });
|
|
522
|
+
},
|
|
513
523
|
createPairing: () => this.#request(() => this.#call("createPairing", {})),
|
|
514
524
|
revokeDevice: (publicKey) => this.#command(() => this.#call("revokeDevice", { publicKey })),
|
|
515
525
|
logout: (providerId) => this.#command(() => {
|
|
@@ -549,6 +559,9 @@ class RemoteClient {
|
|
|
549
559
|
if (!hostSupportsCall(this.#hostMinor, "setPushToken")) {
|
|
550
560
|
throw new Error("The host is too old for this (needs protocol 5.7); run `pinomad upgrade` on the host");
|
|
551
561
|
}
|
|
562
|
+
if (token !== null && typeof token !== "string" && !hostSupportsFeature(this.#hostMinor, "apnsPush")) {
|
|
563
|
+
throw new Error("The host is too old for this (needs protocol 5.9); run `pinomad upgrade` on the host");
|
|
564
|
+
}
|
|
552
565
|
await this.#call("setPushToken", { token });
|
|
553
566
|
})
|
|
554
567
|
};
|
package/dist/pinomad.js
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
packagedVersion
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-1d9sppj0.js";
|
|
5
5
|
import {
|
|
6
6
|
__require
|
|
7
7
|
} from "./chunk-df32avw6.js";
|
|
8
8
|
|
|
9
9
|
// apps/cli/src/pinomad.ts
|
|
10
10
|
var COMMANDS = {
|
|
11
|
-
host: () => import("./chunk-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
join: () => import("./chunk-
|
|
19
|
-
leave: () => import("./chunk-
|
|
11
|
+
host: () => import("./chunk-d4wdt5qs.js"),
|
|
12
|
+
server: () => import("./chunk-4by4x5c4.js"),
|
|
13
|
+
relay: () => import("./chunk-4by4x5c4.js"),
|
|
14
|
+
service: () => import("./chunk-5ng86fmy.js"),
|
|
15
|
+
upgrade: () => import("./chunk-crv3dqzp.js"),
|
|
16
|
+
link: () => import("./chunk-gqr7ahv4.js"),
|
|
17
|
+
pair: () => import("./chunk-zttf1hvh.js"),
|
|
18
|
+
join: () => import("./chunk-9d59jjmt.js"),
|
|
19
|
+
leave: () => import("./chunk-f0v3sht0.js")
|
|
20
20
|
};
|
|
21
|
-
var USAGE = `pinomad — PiNomad host,
|
|
21
|
+
var USAGE = `pinomad — PiNomad host, server, and utilities
|
|
22
22
|
|
|
23
23
|
usage: pinomad <command> [args…]
|
|
24
24
|
|
|
25
25
|
host [host args…] run the host in the foreground
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
server --public-origin O [--data-dir D] [--trust-proxy] run the server
|
|
27
|
+
server passwd [--data-dir D] [--user U] change the account password (stdin)
|
|
28
|
+
relay … old name of server
|
|
28
29
|
service install|uninstall|status|restart|logs user-level host service
|
|
29
30
|
upgrade [--force] [address flags] upgrade the package and restart the service
|
|
30
31
|
link [--data-dir D|--url U …] print the browser link
|
|
31
32
|
pair [--data-dir D|--url U …] print a pairing QR
|
|
32
|
-
relay-id [--data-dir D] print this host's relay id (for --allow-host)
|
|
33
33
|
join <origin> <token> [--data-dir D] join this host to a server account
|
|
34
34
|
leave [--data-dir D] leave the server account
|
|
35
35
|
--version, -v print the version
|
package/package.json
CHANGED
|
Binary file
|