pinomad 0.4.0 → 0.6.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-837gh8nr.js → chunk-0xh6cy9v.js} +69 -17
- package/dist/{chunk-n73t5xzw.js → chunk-367n9je8.js} +2 -2
- package/dist/{chunk-xhwkx9bd.js → chunk-3dnnaz4j.js} +14 -15
- package/dist/{chunk-z5pmcscx.js → chunk-4btj08we.js} +676 -377
- package/dist/{chunk-7gmkgyaz.js → chunk-7qb9tq54.js} +324 -4
- package/dist/{chunk-d4y99pfy.js → chunk-7s1f4z4g.js} +4 -3
- package/dist/chunk-b1h374jr.js +159 -0
- package/dist/{chunk-fgx2sh7d.js → chunk-c8nxym2z.js} +1 -1
- package/dist/{chunk-akgxwg8f.js → chunk-d5ypmmr7.js} +29 -12
- package/dist/{chunk-374zqx5h.js → chunk-d93kvxvc.js} +18 -2
- package/dist/chunk-ggb1taqy.js +16 -0
- package/dist/chunk-jn7y815x.js +79 -0
- package/dist/{chunk-b3f0ced1.js → chunk-k2s1hfq5.js} +39 -16
- package/dist/chunk-kzbzacz9.js +12 -0
- package/dist/{chunk-myjp910s.js → chunk-n4tjhcb7.js} +25 -21
- package/dist/{chunk-cjfmfyjb.js → chunk-ngf1zatm.js} +7 -6
- package/dist/{chunk-vw5yb5pd.js → chunk-wqvp1fwh.js} +304 -38
- package/dist/{chunk-5785kwej.js → chunk-ywmm63ks.js} +9 -7
- package/dist/chunk-znhn8af7.js +33 -0
- package/dist/pinomad.js +40 -16
- package/package.json +1 -1
- package/web/apple-touch-icon.png +0 -0
- package/web/assets/index-C_aU_ZiV.js +383 -0
- package/web/assets/index-DaPcXZPM.css +1 -0
- package/web/favicon.svg +1 -0
- package/web/index.html +4 -2
- package/dist/chunk-17p6bfvy.js +0 -63
- package/dist/chunk-302y6by9.js +0 -50
- package/dist/chunk-knaw1a9g.js +0 -182
- package/dist/chunk-prhvksh3.js +0 -14
- package/dist/chunk-xrvmnymc.js +0 -33
- package/web/assets/index-8mNNPXPT.js +0 -383
- package/web/assets/index-BFJJgGGo.css +0 -1
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HOST_NAME_MAX_UNITS
|
|
3
|
+
} from "./chunk-znhn8af7.js";
|
|
4
|
+
|
|
1
5
|
// packages/protocol/src/relay.ts
|
|
2
6
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
3
7
|
|
|
@@ -290,6 +294,7 @@ function encodeRelayFrame(frame) {
|
|
|
290
294
|
return JSON.stringify(frame);
|
|
291
295
|
}
|
|
292
296
|
var isString2 = (value) => typeof value === "string";
|
|
297
|
+
var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
293
298
|
var isBoundedString = (value, max = 64) => isString2(value) && value.length > 0 && value.length <= max;
|
|
294
299
|
function decodeJson2(text) {
|
|
295
300
|
try {
|
|
@@ -306,7 +311,7 @@ function decodeRelayToHost(text) {
|
|
|
306
311
|
throw new Error("malformed challenge");
|
|
307
312
|
return { t: "challenge", nonce: value.nonce };
|
|
308
313
|
case "registered":
|
|
309
|
-
return
|
|
314
|
+
return decodeRegistered(value);
|
|
310
315
|
case "incoming":
|
|
311
316
|
if (!isString2(value.connId))
|
|
312
317
|
throw new Error("malformed incoming");
|
|
@@ -315,6 +320,8 @@ function decodeRelayToHost(text) {
|
|
|
315
320
|
if (!isBoundedString(value.requestId))
|
|
316
321
|
throw new Error("malformed pairing-request");
|
|
317
322
|
return { t: "pairing-request", requestId: value.requestId };
|
|
323
|
+
case "push-result":
|
|
324
|
+
return decodePushResult(value);
|
|
318
325
|
case "joined":
|
|
319
326
|
if (!isString2(value.username))
|
|
320
327
|
throw new Error("malformed joined");
|
|
@@ -329,11 +336,56 @@ function decodeRelayToHost(text) {
|
|
|
329
336
|
throw new Error("unknown relay frame");
|
|
330
337
|
}
|
|
331
338
|
}
|
|
339
|
+
var isFeatureList = (value) => Array.isArray(value) && value.length <= 16 && value.every((entry) => isString2(entry) && entry.length <= 64);
|
|
340
|
+
function decodeRegistered(value) {
|
|
341
|
+
if (value.features !== undefined && !isFeatureList(value.features))
|
|
342
|
+
throw new Error("malformed registered");
|
|
343
|
+
const declared = value.features === undefined ? {} : { features: value.features };
|
|
344
|
+
if (value.push === undefined)
|
|
345
|
+
return { t: "registered", ...declared };
|
|
346
|
+
if (!Array.isArray(value.push) || value.push.length > 8 || !value.push.every(isString2)) {
|
|
347
|
+
throw new Error("malformed registered");
|
|
348
|
+
}
|
|
349
|
+
const push = value.push.filter((entry) => entry === "production" || entry === "development");
|
|
350
|
+
return { t: "registered", push, ...declared };
|
|
351
|
+
}
|
|
352
|
+
function decodePushResult(value) {
|
|
353
|
+
if (!isBoundedString(value.requestId) || !isBoundedString(value.reason, 256) || value.result !== "accepted" && value.result !== "unregistered" && value.result !== "retry" && value.result !== "rejected" && value.result !== "unavailable") {
|
|
354
|
+
throw new Error("malformed push-result");
|
|
355
|
+
}
|
|
356
|
+
if (value.result === "unregistered") {
|
|
357
|
+
if (typeof value.unregisteredAt !== "number" || !Number.isFinite(value.unregisteredAt)) {
|
|
358
|
+
throw new Error("malformed push-result");
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
t: "push-result",
|
|
362
|
+
requestId: value.requestId,
|
|
363
|
+
result: "unregistered",
|
|
364
|
+
reason: value.reason,
|
|
365
|
+
unregisteredAt: value.unregisteredAt
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (value.result === "retry") {
|
|
369
|
+
if (value.retryAfterMs !== undefined && (typeof value.retryAfterMs !== "number" || !Number.isFinite(value.retryAfterMs))) {
|
|
370
|
+
throw new Error("malformed push-result");
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
t: "push-result",
|
|
374
|
+
requestId: value.requestId,
|
|
375
|
+
result: "retry",
|
|
376
|
+
reason: value.reason,
|
|
377
|
+
...value.retryAfterMs === undefined ? {} : { retryAfterMs: value.retryAfterMs }
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return { t: "push-result", requestId: value.requestId, result: value.result, reason: value.reason };
|
|
381
|
+
}
|
|
332
382
|
function decodeHostToRelay(text) {
|
|
333
383
|
const value = decodeJson2(text);
|
|
334
384
|
switch (value?.t) {
|
|
335
385
|
case "register":
|
|
336
386
|
return decodeRegister(value);
|
|
387
|
+
case "push":
|
|
388
|
+
return decodePush(value);
|
|
337
389
|
case "pairing":
|
|
338
390
|
if (!isBoundedString(value.requestId) || !isBoundedString(value.secret) || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt)) {
|
|
339
391
|
throw new Error("malformed pairing");
|
|
@@ -344,10 +396,33 @@ function decodeHostToRelay(text) {
|
|
|
344
396
|
throw new Error("malformed pairing-refused");
|
|
345
397
|
}
|
|
346
398
|
return { t: "pairing-refused", requestId: value.requestId, reason: value.reason };
|
|
399
|
+
case "name":
|
|
400
|
+
if (!isBoundedString(value.name, HOST_NAME_MAX_UNITS))
|
|
401
|
+
throw new Error("malformed name");
|
|
402
|
+
return { t: "name", name: value.name };
|
|
347
403
|
default:
|
|
348
404
|
throw new Error("unknown host frame");
|
|
349
405
|
}
|
|
350
406
|
}
|
|
407
|
+
function decodePush(value) {
|
|
408
|
+
const n = value.notification;
|
|
409
|
+
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) {
|
|
410
|
+
throw new Error("malformed push");
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
t: "push",
|
|
414
|
+
requestId: value.requestId,
|
|
415
|
+
token: value.token,
|
|
416
|
+
environment: value.environment,
|
|
417
|
+
notification: {
|
|
418
|
+
title: n.title,
|
|
419
|
+
body: n.body,
|
|
420
|
+
collapseId: n.collapseId,
|
|
421
|
+
hostKey: n.hostKey,
|
|
422
|
+
conversationId: n.conversationId
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
351
426
|
function decodeRegister(value) {
|
|
352
427
|
if (value.t !== "register" || !isString2(value.hostId) || !isString2(value.signature)) {
|
|
353
428
|
throw new Error("malformed register");
|
|
@@ -362,9 +437,8 @@ function decodeRegister(value) {
|
|
|
362
437
|
throw new Error("malformed register");
|
|
363
438
|
if (value.name !== undefined && !isString2(value.name))
|
|
364
439
|
throw new Error("malformed register");
|
|
365
|
-
if (value.features !== undefined &&
|
|
440
|
+
if (value.features !== undefined && !isFeatureList(value.features))
|
|
366
441
|
throw new Error("malformed register");
|
|
367
|
-
}
|
|
368
442
|
return {
|
|
369
443
|
t: "register",
|
|
370
444
|
hostId: value.hostId,
|
|
@@ -417,4 +491,250 @@ function verifyRelayChallengeV2(signature, args) {
|
|
|
417
491
|
}
|
|
418
492
|
}
|
|
419
493
|
|
|
420
|
-
|
|
494
|
+
// apps/host/src/relay-link.ts
|
|
495
|
+
import { randomBytes } from "node:crypto";
|
|
496
|
+
import { WebSocket as WebSocket2 } from "ws";
|
|
497
|
+
function relayWsBase(origin) {
|
|
498
|
+
const normalized = origin.replace(/\/+$/, "");
|
|
499
|
+
if (normalized.startsWith("https://"))
|
|
500
|
+
return `wss://${normalized.slice("https://".length)}`;
|
|
501
|
+
if (normalized.startsWith("http://"))
|
|
502
|
+
return `ws://${normalized.slice("http://".length)}`;
|
|
503
|
+
throw new Error(`relay origin must be an http(s) URL: ${origin}`);
|
|
504
|
+
}
|
|
505
|
+
var REJECTION_REASONS = {
|
|
506
|
+
[RELAY_CLOSE.badSignature]: () => `the relay rejected this host's registration signature — a version mismatch?`,
|
|
507
|
+
[RELAY_CLOSE.hostNotAllowed]: (hostId) => `the server does not allow this host (${hostId}); join it to an account with \`pinomad join\``,
|
|
508
|
+
[RELAY_CLOSE.replaced]: () => `the relay replaced this control connection — another process registered the same hostId`
|
|
509
|
+
};
|
|
510
|
+
var PUSH_TIMEOUT_MS = 1e4;
|
|
511
|
+
function startRelayLink(options) {
|
|
512
|
+
const origin = options.origin.replace(/\/+$/, "");
|
|
513
|
+
const wsBase = relayWsBase(origin);
|
|
514
|
+
const hostId = relayHostId(options.signingKey.publicKey);
|
|
515
|
+
const minDelay = options.reconnectDelayMs?.min ?? 1000;
|
|
516
|
+
const maxDelay = options.reconnectDelayMs?.max ?? 30000;
|
|
517
|
+
const maxPendingAccepts = options.maxPendingAccepts ?? 16;
|
|
518
|
+
let state = { status: "connecting" };
|
|
519
|
+
let stopped = false;
|
|
520
|
+
let control;
|
|
521
|
+
let delay = minDelay;
|
|
522
|
+
let retryTimer;
|
|
523
|
+
let pendingAccepts = 0;
|
|
524
|
+
let pushEnvironments;
|
|
525
|
+
let reportedName;
|
|
526
|
+
let nameDeclared = false;
|
|
527
|
+
const pushes = new Map;
|
|
528
|
+
function setState(next) {
|
|
529
|
+
const changed = state.status !== next.status || state.status === "rejected" && next.status === "rejected" && state.reason !== next.reason;
|
|
530
|
+
state = next;
|
|
531
|
+
if (changed)
|
|
532
|
+
options.onStateChange?.(next);
|
|
533
|
+
}
|
|
534
|
+
const watched = new Set;
|
|
535
|
+
const ponged = new WeakMap;
|
|
536
|
+
const pingIntervalMs = options.pingIntervalMs ?? 30000;
|
|
537
|
+
const pingTimer = setInterval(() => {
|
|
538
|
+
for (const socket of watched) {
|
|
539
|
+
if (socket.readyState !== WebSocket2.OPEN)
|
|
540
|
+
continue;
|
|
541
|
+
if (ponged.get(socket) === true) {
|
|
542
|
+
ponged.set(socket, false);
|
|
543
|
+
socket.ping();
|
|
544
|
+
} else {
|
|
545
|
+
socket.terminate();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}, pingIntervalMs);
|
|
549
|
+
function watch(socket) {
|
|
550
|
+
watched.add(socket);
|
|
551
|
+
ponged.set(socket, true);
|
|
552
|
+
socket.on("pong", () => ponged.set(socket, true));
|
|
553
|
+
socket.once("close", () => watched.delete(socket));
|
|
554
|
+
socket.on("error", () => socket.terminate());
|
|
555
|
+
}
|
|
556
|
+
function connectControl() {
|
|
557
|
+
if (stopped)
|
|
558
|
+
return;
|
|
559
|
+
const socket = new WebSocket2(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
|
|
560
|
+
control = socket;
|
|
561
|
+
pushEnvironments = undefined;
|
|
562
|
+
nameDeclared = false;
|
|
563
|
+
reportedName = undefined;
|
|
564
|
+
watch(socket);
|
|
565
|
+
socket.on("message", (data, isBinary) => onControlMessage(socket, data, isBinary));
|
|
566
|
+
socket.once("close", (code) => onControlClosed(socket, code));
|
|
567
|
+
}
|
|
568
|
+
function reconnect(at) {
|
|
569
|
+
if (stopped || retryTimer !== undefined)
|
|
570
|
+
return;
|
|
571
|
+
retryTimer = setTimeout(() => {
|
|
572
|
+
retryTimer = undefined;
|
|
573
|
+
connectControl();
|
|
574
|
+
}, at);
|
|
575
|
+
}
|
|
576
|
+
function onControlMessage(socket, data, isBinary) {
|
|
577
|
+
let frame;
|
|
578
|
+
try {
|
|
579
|
+
if (isBinary)
|
|
580
|
+
throw new Error("binary control frame");
|
|
581
|
+
frame = decodeRelayToHost(data.toString());
|
|
582
|
+
} catch {
|
|
583
|
+
socket.close();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
switch (frame.t) {
|
|
587
|
+
case "challenge": {
|
|
588
|
+
const noisePublicKey = toBase64Url(options.noisePublicKey);
|
|
589
|
+
const challenge = { relayOrigin: origin, hostId, nonce: frame.nonce };
|
|
590
|
+
reportedName = options.hostName?.value;
|
|
591
|
+
socket.send(encodeRelayFrame({
|
|
592
|
+
t: "register",
|
|
593
|
+
hostId,
|
|
594
|
+
signature: signRelayChallenge(options.signingKey.secretKey, challenge),
|
|
595
|
+
signatureV2: signRelayChallengeV2(options.signingKey.secretKey, { ...challenge, noisePublicKey }),
|
|
596
|
+
noisePublicKey,
|
|
597
|
+
...options.accountOffer === undefined ? {} : { features: ["pairing"] },
|
|
598
|
+
...reportedName === undefined ? {} : { name: reportedName }
|
|
599
|
+
}));
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
case "registered": {
|
|
603
|
+
delay = minDelay;
|
|
604
|
+
pushEnvironments = frame.push;
|
|
605
|
+
nameDeclared = frame.features?.includes("name") === true;
|
|
606
|
+
const current = options.hostName?.value;
|
|
607
|
+
if (nameDeclared && current !== undefined && current !== reportedName) {
|
|
608
|
+
reportedName = current;
|
|
609
|
+
socket.send(encodeRelayFrame({ t: "name", name: current }));
|
|
610
|
+
}
|
|
611
|
+
setState({ status: "registered", ...frame.push === undefined ? {} : { push: frame.push } });
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
case "push-result": {
|
|
615
|
+
const request = pushes.get(frame.requestId);
|
|
616
|
+
if (request === undefined || request.socket !== socket)
|
|
617
|
+
break;
|
|
618
|
+
pushes.delete(frame.requestId);
|
|
619
|
+
const { t: _t, requestId: _id, ...verdict } = frame;
|
|
620
|
+
request.settle(verdict);
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
case "pairing-request": {
|
|
624
|
+
const accountOffer = options.accountOffer;
|
|
625
|
+
const refused = (reason) => {
|
|
626
|
+
socket.send(encodeRelayFrame({ t: "pairing-refused", requestId: frame.requestId, reason }));
|
|
627
|
+
};
|
|
628
|
+
if (accountOffer === undefined) {
|
|
629
|
+
refused("unavailable");
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
accountOffer().then((result) => {
|
|
633
|
+
if (socket !== control || socket.readyState !== WebSocket2.OPEN)
|
|
634
|
+
return;
|
|
635
|
+
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 }));
|
|
636
|
+
}, () => {
|
|
637
|
+
if (socket === control && socket.readyState === WebSocket2.OPEN)
|
|
638
|
+
refused("error");
|
|
639
|
+
});
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case "incoming": {
|
|
643
|
+
if (pendingAccepts >= maxPendingAccepts)
|
|
644
|
+
break;
|
|
645
|
+
pendingAccepts += 1;
|
|
646
|
+
const data2 = new WebSocket2(`${wsBase}/accept/${frame.connId}`, { handshakeTimeout: pingIntervalMs });
|
|
647
|
+
watch(data2);
|
|
648
|
+
const release = () => {
|
|
649
|
+
pendingAccepts -= 1;
|
|
650
|
+
clearTimeout(ageOut);
|
|
651
|
+
data2.off("close", release);
|
|
652
|
+
};
|
|
653
|
+
const ageOut = setTimeout(release, 1e4);
|
|
654
|
+
data2.once("close", release);
|
|
655
|
+
options.accept(data2);
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function onControlClosed(socket, code) {
|
|
661
|
+
control = undefined;
|
|
662
|
+
pushEnvironments = undefined;
|
|
663
|
+
nameDeclared = false;
|
|
664
|
+
reportedName = undefined;
|
|
665
|
+
for (const [requestId, request] of pushes) {
|
|
666
|
+
if (request.socket === socket) {
|
|
667
|
+
pushes.delete(requestId);
|
|
668
|
+
request.settle({ result: "retry", reason: "control closed" });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (stopped)
|
|
672
|
+
return;
|
|
673
|
+
const rejection = REJECTION_REASONS[code];
|
|
674
|
+
if (rejection !== undefined) {
|
|
675
|
+
const reason = `relay ${origin}: ${rejection(hostId)} (${code})`;
|
|
676
|
+
setState({ status: "rejected", reason });
|
|
677
|
+
delay = maxDelay;
|
|
678
|
+
reconnect(maxDelay);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
setState({ status: "connecting" });
|
|
682
|
+
reconnect(delay);
|
|
683
|
+
delay = Math.min(maxDelay, delay * 2);
|
|
684
|
+
}
|
|
685
|
+
const detachName = options.hostName?.subscribe((name) => {
|
|
686
|
+
if (name === undefined || name === reportedName)
|
|
687
|
+
return;
|
|
688
|
+
const socket = control;
|
|
689
|
+
if (socket === undefined || socket.readyState !== WebSocket2.OPEN || !nameDeclared)
|
|
690
|
+
return;
|
|
691
|
+
reportedName = name;
|
|
692
|
+
socket.send(encodeRelayFrame({ t: "name", name }));
|
|
693
|
+
});
|
|
694
|
+
connectControl();
|
|
695
|
+
return {
|
|
696
|
+
hostId,
|
|
697
|
+
origin,
|
|
698
|
+
state: () => state,
|
|
699
|
+
sendPush(request) {
|
|
700
|
+
const socket = control;
|
|
701
|
+
if (stopped || socket === undefined || socket.readyState !== WebSocket2.OPEN || state.status !== "registered") {
|
|
702
|
+
return Promise.resolve({ result: "retry", reason: "control disconnected" });
|
|
703
|
+
}
|
|
704
|
+
const envs = pushEnvironments;
|
|
705
|
+
if (envs === undefined) {
|
|
706
|
+
return Promise.resolve({ result: "unavailable", reason: "server does not declare push" });
|
|
707
|
+
}
|
|
708
|
+
if (!envs.includes(request.environment)) {
|
|
709
|
+
return Promise.resolve({ result: "unavailable", reason: `environment ${request.environment} not declared` });
|
|
710
|
+
}
|
|
711
|
+
return new Promise((resolve) => {
|
|
712
|
+
const requestId = randomBytes(16).toString("base64url");
|
|
713
|
+
const timer = setTimeout(() => {
|
|
714
|
+
if (pushes.delete(requestId))
|
|
715
|
+
resolve({ result: "retry", reason: "timeout" });
|
|
716
|
+
}, PUSH_TIMEOUT_MS);
|
|
717
|
+
pushes.set(requestId, {
|
|
718
|
+
socket,
|
|
719
|
+
settle: (verdict) => {
|
|
720
|
+
clearTimeout(timer);
|
|
721
|
+
resolve(verdict);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
socket.send(encodeRelayFrame({ t: "push", requestId, ...request }));
|
|
725
|
+
});
|
|
726
|
+
},
|
|
727
|
+
close() {
|
|
728
|
+
stopped = true;
|
|
729
|
+
if (retryTimer !== undefined)
|
|
730
|
+
clearTimeout(retryTimer);
|
|
731
|
+
clearInterval(pingTimer);
|
|
732
|
+
detachName?.();
|
|
733
|
+
for (const socket of watched)
|
|
734
|
+
socket.terminate();
|
|
735
|
+
return Promise.resolve();
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export { generateKeyPair, keyPairFromPrivate, respondIK, SECURE_PROLOGUE, encodeHostHello, decodeClientHello, toBase64Url, fromBase64Url, SecureSession, RELAY_CLOSE, encodeRelayFrame, decodeRelayToHost, decodeHostToRelay, relayHostId, signRelayChallenge, verifyRelayChallenge, signRelayChallengeV2, verifyRelayChallengeV2, relayWsBase, startRelayLink };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
connectRemoteDurable
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import"./chunk-
|
|
3
|
+
} from "./chunk-d93kvxvc.js";
|
|
4
|
+
import"./chunk-n4tjhcb7.js";
|
|
5
|
+
import"./chunk-znhn8af7.js";
|
|
5
6
|
import {
|
|
6
7
|
hostAddress
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ngf1zatm.js";
|
|
8
9
|
import"./chunk-df32avw6.js";
|
|
9
10
|
|
|
10
11
|
// apps/host/src/cli/pair.ts
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HOST_SETTING_KEYS,
|
|
3
|
+
cleanHostName,
|
|
4
|
+
isQueueMode,
|
|
5
|
+
isThinkingLevel,
|
|
6
|
+
normalizeHostName
|
|
7
|
+
} from "./chunk-znhn8af7.js";
|
|
8
|
+
|
|
9
|
+
// apps/host/src/host-value.ts
|
|
10
|
+
function constantValue(value) {
|
|
11
|
+
return {
|
|
12
|
+
value,
|
|
13
|
+
subscribe: () => () => {}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class Published {
|
|
18
|
+
#listeners = new Set;
|
|
19
|
+
#equal;
|
|
20
|
+
#value;
|
|
21
|
+
constructor(value, equal) {
|
|
22
|
+
this.#value = value;
|
|
23
|
+
this.#equal = equal;
|
|
24
|
+
}
|
|
25
|
+
get value() {
|
|
26
|
+
return this.#value;
|
|
27
|
+
}
|
|
28
|
+
subscribe(listener) {
|
|
29
|
+
this.#listeners.add(listener);
|
|
30
|
+
return () => this.#listeners.delete(listener);
|
|
31
|
+
}
|
|
32
|
+
set(next) {
|
|
33
|
+
if (this.#equal(this.#value, next))
|
|
34
|
+
return;
|
|
35
|
+
this.#value = next;
|
|
36
|
+
for (const listener of this.#listeners)
|
|
37
|
+
listener(next);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
var sameList = (a, b, same) => a.length === b.length && a.every((entry, index) => same(entry, b[index]));
|
|
41
|
+
|
|
42
|
+
// apps/host/src/host-settings.ts
|
|
43
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
44
|
+
import { hostname } from "node:os";
|
|
45
|
+
import { dirname } from "node:path";
|
|
46
|
+
function settingsDefaults(settings) {
|
|
47
|
+
return {
|
|
48
|
+
...settings.defaultModel === null ? {} : { model: settings.defaultModel },
|
|
49
|
+
...settings.defaultThinkingLevel === null ? {} : { thinkingLevel: settings.defaultThinkingLevel }
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
var isModelRef = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.provider === "string" && value.provider !== "" && typeof value.modelId === "string" && value.modelId !== "";
|
|
53
|
+
var fileValueError = (key, value, models) => {
|
|
54
|
+
switch (key) {
|
|
55
|
+
case "defaultModel":
|
|
56
|
+
if (!isModelRef(value))
|
|
57
|
+
return "malformed";
|
|
58
|
+
return models.some((each) => each.provider === value.provider && each.modelId === value.modelId) ? undefined : "unavailable";
|
|
59
|
+
case "defaultThinkingLevel":
|
|
60
|
+
return isThinkingLevel(value) ? undefined : "malformed";
|
|
61
|
+
case "steeringMode":
|
|
62
|
+
case "followUpMode":
|
|
63
|
+
return isQueueMode(value) ? undefined : "malformed";
|
|
64
|
+
case "acceptAccountDevices":
|
|
65
|
+
return typeof value === "boolean" ? undefined : "malformed";
|
|
66
|
+
case "hostName":
|
|
67
|
+
return typeof value === "string" ? undefined : "malformed";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var sameModel = (a, b) => a === b || a !== null && b !== null && a.provider === b.provider && a.modelId === b.modelId;
|
|
71
|
+
var sameSettings = (a, b) => a.steeringMode === b.steeringMode && a.followUpMode === b.followUpMode && a.acceptAccountDevices === b.acceptAccountDevices && a.defaultThinkingLevel === b.defaultThinkingLevel && sameModel(a.defaultModel, b.defaultModel) && a.hostName === b.hostName && sameList(a.custom, b.custom, (x, y) => x === y);
|
|
72
|
+
var parse = (path, text) => {
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(text);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error(`Invalid settings file ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
78
|
+
}
|
|
79
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
80
|
+
throw new Error(`Invalid settings file ${path}: expected a JSON object`);
|
|
81
|
+
}
|
|
82
|
+
return parsed;
|
|
83
|
+
};
|
|
84
|
+
async function openHostSettings(options) {
|
|
85
|
+
const read = () => readFile(options.path, "utf8").then((text) => parse(options.path, text), (error) => {
|
|
86
|
+
if (error.code === "ENOENT")
|
|
87
|
+
return {};
|
|
88
|
+
throw new Error(`Cannot read settings file ${options.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
});
|
|
90
|
+
let file = await read();
|
|
91
|
+
const warned = new Set;
|
|
92
|
+
const compute = () => {
|
|
93
|
+
const fileValues = {};
|
|
94
|
+
const custom = [];
|
|
95
|
+
for (const key of HOST_SETTING_KEYS) {
|
|
96
|
+
if (!Object.hasOwn(file, key))
|
|
97
|
+
continue;
|
|
98
|
+
const value = file[key];
|
|
99
|
+
const invalid = fileValueError(key, value, options.models.value);
|
|
100
|
+
if (invalid === "malformed" && !warned.has(key)) {
|
|
101
|
+
warned.add(key);
|
|
102
|
+
console.error(`${options.path}: ignoring invalid value for settings key "${key}": ${JSON.stringify(value)}`);
|
|
103
|
+
}
|
|
104
|
+
if (invalid !== undefined)
|
|
105
|
+
continue;
|
|
106
|
+
fileValues[key] = value;
|
|
107
|
+
custom.push(key);
|
|
108
|
+
}
|
|
109
|
+
const { fallback } = options;
|
|
110
|
+
return {
|
|
111
|
+
defaultModel: fileValues.defaultModel ?? fallback.defaultModel(),
|
|
112
|
+
defaultThinkingLevel: fileValues.defaultThinkingLevel ?? fallback.defaultThinkingLevel(),
|
|
113
|
+
steeringMode: fileValues.steeringMode ?? fallback.steeringMode(),
|
|
114
|
+
followUpMode: fileValues.followUpMode ?? fallback.followUpMode(),
|
|
115
|
+
acceptAccountDevices: fileValues.acceptAccountDevices ?? true,
|
|
116
|
+
hostName: normalizeHostName(fileValues.hostName ?? hostname()),
|
|
117
|
+
custom
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
const published = new Published(compute(), sameSettings);
|
|
121
|
+
options.models.subscribe(() => published.set(compute()));
|
|
122
|
+
let tail = Promise.resolve();
|
|
123
|
+
const write = async (patch) => {
|
|
124
|
+
const model = patch.defaultModel;
|
|
125
|
+
if (model !== undefined && model !== null && !options.models.value.some((each) => each.provider === model.provider && each.modelId === model.modelId)) {
|
|
126
|
+
throw new Error(`Unknown model: ${model.provider}/${model.modelId}`);
|
|
127
|
+
}
|
|
128
|
+
const name = patch.hostName;
|
|
129
|
+
if (name !== undefined && name !== null && cleanHostName(name) === "") {
|
|
130
|
+
throw new Error(`Invalid host name: ${JSON.stringify(name)}`);
|
|
131
|
+
}
|
|
132
|
+
const next = { ...await read() };
|
|
133
|
+
for (const key of HOST_SETTING_KEYS) {
|
|
134
|
+
const value = patch[key];
|
|
135
|
+
if (value === undefined)
|
|
136
|
+
continue;
|
|
137
|
+
if (value === null)
|
|
138
|
+
delete next[key];
|
|
139
|
+
else
|
|
140
|
+
next[key] = key === "hostName" ? cleanHostName(value) : value;
|
|
141
|
+
}
|
|
142
|
+
const tmp = `${options.path}.tmp`;
|
|
143
|
+
await mkdir(dirname(options.path), { recursive: true });
|
|
144
|
+
await writeFile(tmp, `${JSON.stringify(next, null, 2)}
|
|
145
|
+
`, { mode: 384 });
|
|
146
|
+
await rename(tmp, options.path);
|
|
147
|
+
file = next;
|
|
148
|
+
published.set(compute());
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
get value() {
|
|
152
|
+
return published.value;
|
|
153
|
+
},
|
|
154
|
+
subscribe: (listener) => published.subscribe(listener),
|
|
155
|
+
update: (patch) => tail = tail.then(() => write(patch), () => write(patch))
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { constantValue, Published, sameList, settingsDefaults, openHostSettings };
|
|
@@ -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.6.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}`;
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isParseArgsError
|
|
3
|
+
} from "./chunk-kzbzacz9.js";
|
|
1
4
|
import {
|
|
2
5
|
commandHint,
|
|
3
6
|
hostEntry,
|
|
4
7
|
packagedVersion
|
|
5
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-c8nxym2z.js";
|
|
6
9
|
import {
|
|
10
|
+
ProtocolMismatchError,
|
|
7
11
|
connectRemoteDurable
|
|
8
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-d93kvxvc.js";
|
|
9
13
|
import {
|
|
10
14
|
DEFAULT_DATA_DIR,
|
|
11
15
|
hostAddress
|
|
12
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-ngf1zatm.js";
|
|
13
17
|
|
|
14
18
|
// apps/host/src/cli/service.ts
|
|
15
19
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -375,7 +379,7 @@ function serviceManager(platform = process.platform) {
|
|
|
375
379
|
var run2 = promisify2(execFile2);
|
|
376
380
|
var repoRoot = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
377
381
|
var message = (error) => error instanceof Error ? error.message : String(error);
|
|
378
|
-
function
|
|
382
|
+
function connectToHost(url, token, timeoutMs = 3000) {
|
|
379
383
|
const connecting = connectRemoteDurable({ url, token, reconnectDelayMs: { min: 200, max: 2000 } });
|
|
380
384
|
const timedOut = Symbol("timedOut");
|
|
381
385
|
return Promise.race([
|
|
@@ -416,7 +420,7 @@ async function status(argv) {
|
|
|
416
420
|
}
|
|
417
421
|
try {
|
|
418
422
|
const { url, token } = await hostAddress(argv);
|
|
419
|
-
const remote = await
|
|
423
|
+
const remote = await connectToHost(url, token);
|
|
420
424
|
if (remote.view.current().tasks === undefined)
|
|
421
425
|
await remote.controller.toggleTasks();
|
|
422
426
|
const tasks = remote.view.current().tasks;
|
|
@@ -424,6 +428,8 @@ async function status(argv) {
|
|
|
424
428
|
console.log(`host: reachable at ${url}, ${count} live task${count === 1 ? "" : "s"}`);
|
|
425
429
|
remote.close();
|
|
426
430
|
} catch (error) {
|
|
431
|
+
if (isParseArgsError(error))
|
|
432
|
+
throw error;
|
|
427
433
|
console.log(`host: unreachable (${message(error)})`);
|
|
428
434
|
}
|
|
429
435
|
}
|
|
@@ -431,13 +437,18 @@ async function restartService({
|
|
|
431
437
|
force,
|
|
432
438
|
argv,
|
|
433
439
|
manager = serviceManager(),
|
|
434
|
-
restart
|
|
440
|
+
restart,
|
|
441
|
+
connect = connectToHost,
|
|
442
|
+
pollTimeoutMs = 30000,
|
|
443
|
+
pollDelayMs = 500
|
|
435
444
|
}) {
|
|
436
445
|
if (!(await manager.state()).installed) {
|
|
437
446
|
console.log(`service not installed (${commandHint("service")} install)`);
|
|
438
447
|
return 1;
|
|
439
448
|
}
|
|
440
|
-
const address = await hostAddress(argv).catch(() => {
|
|
449
|
+
const address = await hostAddress(argv).catch((error) => {
|
|
450
|
+
if (isParseArgsError(error))
|
|
451
|
+
throw error;
|
|
441
452
|
return;
|
|
442
453
|
});
|
|
443
454
|
if (!force && address !== undefined) {
|
|
@@ -457,7 +468,7 @@ async function restartService({
|
|
|
457
468
|
console.log("restart requested (host address unknown; not polling)");
|
|
458
469
|
return 0;
|
|
459
470
|
}
|
|
460
|
-
const deadline = Date.now() +
|
|
471
|
+
const deadline = Date.now() + pollTimeoutMs;
|
|
461
472
|
for (;; ) {
|
|
462
473
|
try {
|
|
463
474
|
const remote = await connect(address.url, address.token, 2000);
|
|
@@ -465,11 +476,16 @@ async function restartService({
|
|
|
465
476
|
console.log(`host restarted, reachable at ${address.url}`);
|
|
466
477
|
return 0;
|
|
467
478
|
} catch (error) {
|
|
479
|
+
if (error instanceof ProtocolMismatchError && error.direction === "client-older") {
|
|
480
|
+
const newer = error.hostVersion ?? `protocol major ${error.hostMajor}`;
|
|
481
|
+
console.log(`host restarted (${newer}, newer than this upgrader), reachable at ${address.url}`);
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
468
484
|
if (Date.now() > deadline) {
|
|
469
|
-
console.error(`host did not answer within
|
|
485
|
+
console.error(`host did not answer within ${pollTimeoutMs / 1000}s after restart: ${message(error)}`);
|
|
470
486
|
return 1;
|
|
471
487
|
}
|
|
472
|
-
await new Promise((resolve3) => setTimeout(resolve3,
|
|
488
|
+
await new Promise((resolve3) => setTimeout(resolve3, pollDelayMs));
|
|
473
489
|
}
|
|
474
490
|
}
|
|
475
491
|
}
|
|
@@ -512,11 +528,12 @@ async function main(argv) {
|
|
|
512
528
|
process.exitCode = 1;
|
|
513
529
|
}
|
|
514
530
|
} catch (error) {
|
|
531
|
+
if (isParseArgsError(error))
|
|
532
|
+
throw error;
|
|
515
533
|
console.error(message(error));
|
|
516
534
|
process.exitCode = 1;
|
|
517
535
|
}
|
|
518
536
|
}
|
|
519
|
-
if (false)
|
|
520
|
-
;
|
|
537
|
+
if (false) {}
|
|
521
538
|
|
|
522
539
|
export { installSpec, argValue, withoutRelayArgs, serviceManager, restartService, main };
|