pinomad 0.5.0 → 0.7.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.
Files changed (31) hide show
  1. package/README.md +2 -2
  2. package/dist/{chunk-f0v3sht0.js → chunk-07769pqx.js} +10 -8
  3. package/dist/{chunk-ebfaf3k2.js → chunk-0xh6cy9v.js} +1 -1
  4. package/dist/{chunk-aqj4wg33.js → chunk-12kaxggg.js} +29 -12
  5. package/dist/{chunk-d4wdt5qs.js → chunk-1nyzr0mg.js} +2282 -1027
  6. package/dist/chunk-1t58fh0k.js +16 -0
  7. package/dist/{chunk-4by4x5c4.js → chunk-6ysezfy5.js} +44 -14
  8. package/dist/{chunk-kmbh44dt.js → chunk-7qb9tq54.js} +42 -6
  9. package/dist/{chunk-gqr7ahv4.js → chunk-8yme2wep.js} +2 -2
  10. package/dist/chunk-b1h374jr.js +159 -0
  11. package/dist/{chunk-hsp12cjt.js → chunk-e4ahjbmz.js} +212 -50
  12. package/dist/{chunk-9d59jjmt.js → chunk-fbkd73zr.js} +36 -10
  13. package/dist/chunk-kzbzacz9.js +12 -0
  14. package/dist/{chunk-zttf1hvh.js → chunk-m4c5gvce.js} +4 -3
  15. package/dist/{chunk-cjfmfyjb.js → chunk-ngf1zatm.js} +7 -6
  16. package/dist/{chunk-1d9sppj0.js → chunk-rtwxjb90.js} +1 -1
  17. package/dist/{chunk-m4q9wafw.js → chunk-sj7rnvh4.js} +118 -6
  18. package/dist/chunk-sz3rzk55.js +79 -0
  19. package/dist/{chunk-26d04z02.js → chunk-ywmm63ks.js} +7 -3
  20. package/dist/chunk-znhn8af7.js +33 -0
  21. package/dist/pinomad.js +36 -12
  22. package/package.json +1 -1
  23. package/web/assets/index-C0rwAo0H.css +1 -0
  24. package/web/assets/index-DGee2kNU.js +394 -0
  25. package/web/index.html +2 -2
  26. package/dist/chunk-5ng86fmy.js +0 -14
  27. package/dist/chunk-crv3dqzp.js +0 -63
  28. package/web/assets/index-BkF0-im5.css +0 -1
  29. package/web/assets/index-D4lgm3Y4.js +0 -383
  30. package/web/assets/montserrat-latin-ext-wght-normal-BsZE-iaG.woff2 +0 -0
  31. package/web/assets/montserrat-latin-wght-normal-l_AIctKy.woff2 +0 -0
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ main,
4
+ restartService
5
+ } from "./chunk-12kaxggg.js";
6
+ import"./chunk-kzbzacz9.js";
7
+ import"./chunk-rtwxjb90.js";
8
+ import"./chunk-sj7rnvh4.js";
9
+ import"./chunk-e4ahjbmz.js";
10
+ import"./chunk-znhn8af7.js";
11
+ import"./chunk-ngf1zatm.js";
12
+ import"./chunk-df32avw6.js";
13
+ export {
14
+ restartService,
15
+ main
16
+ };
@@ -9,11 +9,14 @@ import {
9
9
  relayWsBase,
10
10
  verifyRelayChallenge,
11
11
  verifyRelayChallengeV2
12
- } from "./chunk-kmbh44dt.js";
12
+ } from "./chunk-7qb9tq54.js";
13
13
  import {
14
14
  commandHint,
15
15
  webRoot
16
- } from "./chunk-1d9sppj0.js";
16
+ } from "./chunk-rtwxjb90.js";
17
+ import {
18
+ normalizeHostName
19
+ } from "./chunk-znhn8af7.js";
17
20
  import"./chunk-df32avw6.js";
18
21
 
19
22
  // apps/server/src/main.ts
@@ -325,7 +328,7 @@ async function openAccounts(options) {
325
328
  db.exec("ROLLBACK");
326
329
  return { status: "refused", reason: failure };
327
330
  }
328
- const hostName = name === undefined || name.trim() === "" ? "Unknown host" : name.trim().slice(0, 64);
331
+ const hostName = name === undefined ? "Unknown host" : normalizeHostName(name);
329
332
  db.prepare(`INSERT INTO hosts (host_id, user_id, noise_public_key, name, joined_at, last_seen_at)
330
333
  VALUES (?, ?, ?, ?, ?, ?)
331
334
  ON CONFLICT(host_id) DO UPDATE SET
@@ -358,6 +361,9 @@ async function openAccounts(options) {
358
361
  markHostSeen(hostId) {
359
362
  db.prepare("UPDATE hosts SET last_seen_at = ? WHERE host_id = ?").run(now(), hostId);
360
363
  },
364
+ renameHost(hostId, name) {
365
+ db.prepare("UPDATE hosts SET name = ? WHERE host_id = ?").run(normalizeHostName(name), hostId);
366
+ },
361
367
  async close() {
362
368
  db.close();
363
369
  await release();
@@ -396,7 +402,7 @@ async function changePassword(options) {
396
402
 
397
403
  // apps/server/src/apns.ts
398
404
  import { createPrivateKey, sign } from "node:crypto";
399
- import { connect } from "node:http2";
405
+ import { connect, constants } from "node:http2";
400
406
  var AUTHORITIES = {
401
407
  production: "https://api.push.apple.com",
402
408
  development: "https://api.sandbox.push.apple.com"
@@ -465,19 +471,25 @@ function openApns(config) {
465
471
  state.token = { jwt, issuedAt: now };
466
472
  return jwt;
467
473
  };
474
+ const dropSession = (state, session) => {
475
+ if (state.session === session)
476
+ state.session = undefined;
477
+ if (!session.destroyed)
478
+ session.destroy();
479
+ };
480
+ const retireSession = (state, session) => {
481
+ if (state.session === session)
482
+ state.session = undefined;
483
+ if (!session.closed && !session.destroyed)
484
+ session.close();
485
+ };
468
486
  const sessionFor = (environment, state) => {
469
487
  const existing = state.session;
470
488
  if (existing !== undefined && !existing.destroyed && !existing.closed)
471
489
  return existing;
472
490
  const session = connect(state.authority);
473
- const drop = () => {
474
- if (state.session === session)
475
- state.session = undefined;
476
- if (!session.destroyed)
477
- session.destroy();
478
- };
479
- session.once("goaway", drop);
480
- session.once("error", drop);
491
+ session.once("goaway", () => dropSession(state, session));
492
+ session.once("error", () => dropSession(state, session));
481
493
  session.once("close", () => {
482
494
  if (state.session === session)
483
495
  state.session = undefined;
@@ -560,6 +572,7 @@ function openApns(config) {
560
572
  resolve({ result: "retry", reason: "session unavailable" });
561
573
  return;
562
574
  }
575
+ let responded = false;
563
576
  let status = 0;
564
577
  let responseHeaders = {};
565
578
  const chunks = [];
@@ -574,11 +587,16 @@ function openApns(config) {
574
587
  };
575
588
  const timer = setTimeout(() => finish({ result: "retry", reason: "apns timeout" }), REQUEST_TIMEOUT_MS);
576
589
  stream.on("response", (headers) => {
590
+ responded = true;
577
591
  status = Number(headers[":status"] ?? 0);
578
592
  responseHeaders = headers;
579
593
  });
580
594
  stream.on("data", (chunk) => chunks.push(chunk));
581
595
  stream.on("end", () => {
596
+ if (!responded) {
597
+ finish({ result: "retry", reason: "connection closed" });
598
+ return;
599
+ }
582
600
  let body2;
583
601
  try {
584
602
  body2 = JSON.parse(Buffer.concat(chunks).toString("utf8"));
@@ -587,7 +605,12 @@ function openApns(config) {
587
605
  }
588
606
  finish(classify(request.environment, status, responseHeaders, body2, state));
589
607
  });
590
- stream.on("error", () => finish({ result: "retry", reason: "connection error" }));
608
+ stream.on("error", () => {
609
+ const refused = stream.rstCode === constants.NGHTTP2_REFUSED_STREAM;
610
+ finish({ result: "retry", reason: "connection error" });
611
+ if (refused)
612
+ retireSession(state, session);
613
+ });
591
614
  stream.on("aborted", () => finish({ result: "retry", reason: "connection aborted" }));
592
615
  stream.end(body);
593
616
  });
@@ -1041,6 +1064,11 @@ async function startRelay(options) {
1041
1064
  handlePush(socket, hostId, answer);
1042
1065
  return;
1043
1066
  }
1067
+ if (answer.t === "name") {
1068
+ if (controls.get(hostId)?.socket === socket)
1069
+ accounts?.renameHost(hostId, answer.name);
1070
+ return;
1071
+ }
1044
1072
  if (answer.t === "pairing" || answer.t === "pairing-refused") {
1045
1073
  const request = pairingRequests.get(answer.requestId);
1046
1074
  if (request === undefined || request.control !== socket)
@@ -1097,7 +1125,9 @@ async function startRelay(options) {
1097
1125
  ...frame.features === undefined ? {} : { features: frame.features }
1098
1126
  });
1099
1127
  accounts?.markHostSeen(hostId);
1100
- socket.send(encodeRelayFrame({ t: "registered", push: pushEnvironments }));
1128
+ if (frame.name !== undefined)
1129
+ accounts?.renameHost(hostId, frame.name);
1130
+ socket.send(encodeRelayFrame({ t: "registered", push: pushEnvironments, features: ["name"] }));
1101
1131
  });
1102
1132
  socket.once("close", () => {
1103
1133
  clearTimeout(deadline);
@@ -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
 
@@ -332,14 +336,18 @@ function decodeRelayToHost(text) {
332
336
  throw new Error("unknown relay frame");
333
337
  }
334
338
  }
339
+ var isFeatureList = (value) => Array.isArray(value) && value.length <= 16 && value.every((entry) => isString2(entry) && entry.length <= 64);
335
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 };
336
344
  if (value.push === undefined)
337
- return { t: "registered" };
345
+ return { t: "registered", ...declared };
338
346
  if (!Array.isArray(value.push) || value.push.length > 8 || !value.push.every(isString2)) {
339
347
  throw new Error("malformed registered");
340
348
  }
341
349
  const push = value.push.filter((entry) => entry === "production" || entry === "development");
342
- return { t: "registered", push };
350
+ return { t: "registered", push, ...declared };
343
351
  }
344
352
  function decodePushResult(value) {
345
353
  if (!isBoundedString(value.requestId) || !isBoundedString(value.reason, 256) || value.result !== "accepted" && value.result !== "unregistered" && value.result !== "retry" && value.result !== "rejected" && value.result !== "unavailable") {
@@ -388,6 +396,10 @@ function decodeHostToRelay(text) {
388
396
  throw new Error("malformed pairing-refused");
389
397
  }
390
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 };
391
403
  default:
392
404
  throw new Error("unknown host frame");
393
405
  }
@@ -425,9 +437,8 @@ function decodeRegister(value) {
425
437
  throw new Error("malformed register");
426
438
  if (value.name !== undefined && !isString2(value.name))
427
439
  throw new Error("malformed register");
428
- if (value.features !== undefined && (!Array.isArray(value.features) || value.features.length > 16 || !value.features.every((feature) => isString2(feature) && feature.length <= 64))) {
440
+ if (value.features !== undefined && !isFeatureList(value.features))
429
441
  throw new Error("malformed register");
430
- }
431
442
  return {
432
443
  t: "register",
433
444
  hostId: value.hostId,
@@ -511,6 +522,8 @@ function startRelayLink(options) {
511
522
  let retryTimer;
512
523
  let pendingAccepts = 0;
513
524
  let pushEnvironments;
525
+ let reportedName;
526
+ let nameDeclared = false;
514
527
  const pushes = new Map;
515
528
  function setState(next) {
516
529
  const changed = state.status !== next.status || state.status === "rejected" && next.status === "rejected" && state.reason !== next.reason;
@@ -546,6 +559,8 @@ function startRelayLink(options) {
546
559
  const socket = new WebSocket2(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
547
560
  control = socket;
548
561
  pushEnvironments = undefined;
562
+ nameDeclared = false;
563
+ reportedName = undefined;
549
564
  watch(socket);
550
565
  socket.on("message", (data, isBinary) => onControlMessage(socket, data, isBinary));
551
566
  socket.once("close", (code) => onControlClosed(socket, code));
@@ -572,21 +587,30 @@ function startRelayLink(options) {
572
587
  case "challenge": {
573
588
  const noisePublicKey = toBase64Url(options.noisePublicKey);
574
589
  const challenge = { relayOrigin: origin, hostId, nonce: frame.nonce };
590
+ reportedName = options.hostName?.value;
575
591
  socket.send(encodeRelayFrame({
576
592
  t: "register",
577
593
  hostId,
578
594
  signature: signRelayChallenge(options.signingKey.secretKey, challenge),
579
595
  signatureV2: signRelayChallengeV2(options.signingKey.secretKey, { ...challenge, noisePublicKey }),
580
596
  noisePublicKey,
581
- ...options.accountOffer === undefined ? {} : { features: ["pairing"] }
597
+ ...options.accountOffer === undefined ? {} : { features: ["pairing"] },
598
+ ...reportedName === undefined ? {} : { name: reportedName }
582
599
  }));
583
600
  break;
584
601
  }
585
- case "registered":
602
+ case "registered": {
586
603
  delay = minDelay;
587
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
+ }
588
611
  setState({ status: "registered", ...frame.push === undefined ? {} : { push: frame.push } });
589
612
  break;
613
+ }
590
614
  case "push-result": {
591
615
  const request = pushes.get(frame.requestId);
592
616
  if (request === undefined || request.socket !== socket)
@@ -636,6 +660,8 @@ function startRelayLink(options) {
636
660
  function onControlClosed(socket, code) {
637
661
  control = undefined;
638
662
  pushEnvironments = undefined;
663
+ nameDeclared = false;
664
+ reportedName = undefined;
639
665
  for (const [requestId, request] of pushes) {
640
666
  if (request.socket === socket) {
641
667
  pushes.delete(requestId);
@@ -656,6 +682,15 @@ function startRelayLink(options) {
656
682
  reconnect(delay);
657
683
  delay = Math.min(maxDelay, delay * 2);
658
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
+ });
659
694
  connectControl();
660
695
  return {
661
696
  hostId,
@@ -694,6 +729,7 @@ function startRelayLink(options) {
694
729
  if (retryTimer !== undefined)
695
730
  clearTimeout(retryTimer);
696
731
  clearInterval(pingTimer);
732
+ detachName?.();
697
733
  for (const socket of watched)
698
734
  socket.terminate();
699
735
  return Promise.resolve();
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  packagedVersion
3
- } from "./chunk-1d9sppj0.js";
3
+ } from "./chunk-rtwxjb90.js";
4
4
  import {
5
5
  hostAddress
6
- } from "./chunk-cjfmfyjb.js";
6
+ } from "./chunk-ngf1zatm.js";
7
7
  import"./chunk-df32avw6.js";
8
8
 
9
9
  // apps/host/src/cli/web-link.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 };