pinomad 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,176 @@ import {
2
2
  HOST_SETTING_KEYS,
3
3
  isQueueMode,
4
4
  isThinkingLevel
5
- } from "./chunk-sygeb9qb.js";
5
+ } from "./chunk-pqe7q1s7.js";
6
+
7
+ // packages/protocol/src/rtc.ts
8
+ function rtcAddressScope(address) {
9
+ const ip = parseRtcAddress(address);
10
+ if (ip === undefined)
11
+ return "unknown";
12
+ if (ip.family === 4)
13
+ return ipv4Scope(ip.octets);
14
+ const octets = mappedIpv4(ip.segments);
15
+ return octets === undefined ? ipv6Scope(ip.segments) : ipv4Scope(octets);
16
+ }
17
+ function rtcCandidateEnd(info) {
18
+ return {
19
+ ...info.type === undefined ? {} : { type: info.type },
20
+ ...info.address === undefined ? {} : { address: info.address },
21
+ scope: rtcAddressScope(info.address)
22
+ };
23
+ }
24
+ function rtcSelectedPair(local, remote) {
25
+ return { local: rtcCandidateEnd(local), remote: rtcCandidateEnd(remote) };
26
+ }
27
+ function parseRtcAddress(address) {
28
+ if (address === undefined)
29
+ return;
30
+ const text = address.trim();
31
+ if (text === "")
32
+ return;
33
+ let host = text;
34
+ if (text.startsWith("[")) {
35
+ const close = text.indexOf("]");
36
+ if (close === -1)
37
+ return;
38
+ const tail = text.slice(close + 1);
39
+ if (tail !== "" && !/^:\d+$/.test(tail))
40
+ return;
41
+ host = text.slice(1, close);
42
+ } else if (text.split(":").length === 2) {
43
+ const [candidate, port] = text.split(":");
44
+ if (/^\d+$/.test(port))
45
+ host = candidate;
46
+ }
47
+ const zone = host.indexOf("%");
48
+ if (zone !== -1)
49
+ host = host.slice(0, zone);
50
+ const ipv4 = parseIpv4(host);
51
+ if (ipv4 !== undefined)
52
+ return { family: 4, octets: ipv4 };
53
+ const ipv6 = parseIpv6(host);
54
+ return ipv6 === undefined ? undefined : { family: 6, segments: ipv6 };
55
+ }
56
+ function parseIpv4(text) {
57
+ const parts = text.split(".");
58
+ if (parts.length !== 4)
59
+ return;
60
+ const octets = [];
61
+ for (const part of parts) {
62
+ if (!/^(?:0|[1-9]\d{0,2})$/.test(part))
63
+ return;
64
+ const value = Number(part);
65
+ if (value > 255)
66
+ return;
67
+ octets.push(value);
68
+ }
69
+ return octets;
70
+ }
71
+ function parseIpv6(text) {
72
+ const halves = text.split("::");
73
+ if (halves.length > 2)
74
+ return;
75
+ const parsePart = (part, allowIpv4) => {
76
+ if (part === "")
77
+ return [];
78
+ const tokens = part.split(":");
79
+ const groups = [];
80
+ for (let index = 0;index < tokens.length; index++) {
81
+ const token = tokens[index];
82
+ if (allowIpv4 && index === tokens.length - 1 && token.includes(".")) {
83
+ const v4 = parseIpv4(token);
84
+ if (v4 === undefined)
85
+ return;
86
+ groups.push(v4[0] << 8 | v4[1], v4[2] << 8 | v4[3]);
87
+ continue;
88
+ }
89
+ if (!/^[0-9a-fA-F]{1,4}$/.test(token))
90
+ return;
91
+ groups.push(Number.parseInt(token, 16));
92
+ }
93
+ return groups;
94
+ };
95
+ if (halves.length === 1) {
96
+ const groups = parsePart(text, true);
97
+ return groups?.length === 8 ? groups : undefined;
98
+ }
99
+ const left = parsePart(halves[0], false);
100
+ const right = parsePart(halves[1], true);
101
+ if (left === undefined || right === undefined)
102
+ return;
103
+ const zeros = 8 - left.length - right.length;
104
+ if (zeros < 1)
105
+ return;
106
+ return [...left, ...new Array(zeros).fill(0), ...right];
107
+ }
108
+ function mappedIpv4(segments) {
109
+ if (!segments.slice(0, 5).every((group) => group === 0) || segments[5] !== 65535)
110
+ return;
111
+ return [segments[6] >> 8, segments[6] & 255, segments[7] >> 8, segments[7] & 255];
112
+ }
113
+ function ipv4Scope(octets) {
114
+ const [a, b] = octets;
115
+ if (a === 127)
116
+ return "loopback";
117
+ if (a === 100 && b >= 64 && b <= 127)
118
+ return "tailnet";
119
+ if (a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254) {
120
+ return "lan";
121
+ }
122
+ return "public";
123
+ }
124
+ function ipv6Scope(segments) {
125
+ if (segments[0] === 64890 && segments[1] === 4444 && segments[2] === 41440)
126
+ return "tailnet";
127
+ if ((segments[0] & 65024) === 64512 || (segments[0] & 65472) === 65152)
128
+ return "lan";
129
+ if (segments.slice(0, 7).every((group) => group === 0) && segments[7] === 1)
130
+ return "loopback";
131
+ return "public";
132
+ }
133
+ var RTC_DEFAULT_STUN = ["stun:stun.cloudflare.com:3478"];
134
+ var RTC_DATA_CHANNEL_LABEL = "pinomad";
135
+ var RTC_MAX_RECORD_BYTES = 65535;
136
+ var RTC_DEFAULT_MAX_MESSAGE_SIZE = 65536;
137
+ var RTC_MIN_MAX_MESSAGE_SIZE = RTC_MAX_RECORD_BYTES;
138
+ function isRtcHello(value) {
139
+ if (typeof value !== "object" || value === null || Array.isArray(value))
140
+ return false;
141
+ const hello = value;
142
+ return Array.isArray(hello.stun) && hello.stun.every((server) => typeof server === "string" && server.length > 0) && hello.signalOnly === true;
143
+ }
144
+ function rtcCandidateType(candidate) {
145
+ const parts = candidate.trim().split(/\s+/);
146
+ const at = parts.indexOf("typ");
147
+ return at === -1 ? undefined : parts[at + 1];
148
+ }
149
+ function sdpCandidates(sdp) {
150
+ return sdp.split(/\r?\n/).filter((line) => line.startsWith("a=candidate:")).map((line) => line.slice("a=".length));
151
+ }
152
+ function withRtcCandidates(sdp, candidates) {
153
+ const lines = sdp.split(/\r?\n/).filter((line) => line !== "" && !line.startsWith("a=candidate:") && line !== "a=end-of-candidates");
154
+ const unique = [...new Set(candidates.map((candidate) => candidate.replace(/^a=/, "")))];
155
+ return [...lines, ...unique.map((candidate) => `a=${candidate}`), "a=end-of-candidates", ""].join(`\r
156
+ `);
157
+ }
158
+ function parseRtcMaxMessageSize(sdp) {
159
+ const line = sdp.split(/\r?\n/).find((entry) => entry.startsWith("a=max-message-size:"));
160
+ if (line === undefined)
161
+ return { kind: "bounded", bytes: RTC_DEFAULT_MAX_MESSAGE_SIZE };
162
+ const raw = line.slice("a=max-message-size:".length).trim();
163
+ if (!/^\d+$/.test(raw))
164
+ return { kind: "invalid", reason: `malformed a=max-message-size: ${raw}` };
165
+ const bytes = Number(raw);
166
+ if (!Number.isSafeInteger(bytes))
167
+ return { kind: "invalid", reason: `a=max-message-size is not a safe integer: ${raw}` };
168
+ if (bytes === 0)
169
+ return { kind: "unlimited" };
170
+ if (bytes < RTC_MIN_MAX_MESSAGE_SIZE) {
171
+ return { kind: "invalid", reason: `a=max-message-size ${bytes} cannot carry a ${RTC_MIN_MAX_MESSAGE_SIZE}-byte record` };
172
+ }
173
+ return { kind: "bounded", bytes };
174
+ }
6
175
 
7
176
  // packages/protocol/src/auth.ts
8
177
  var CUSTOM_PROVIDER_APIS = ["openai-completions", "openai-responses", "anthropic-messages", "google-generative-ai"];
@@ -218,175 +387,6 @@ function nonempty(value) {
218
387
  return typeof value === "string" && value.length > 0;
219
388
  }
220
389
 
221
- // packages/protocol/src/rtc.ts
222
- function rtcAddressScope(address) {
223
- const ip = parseRtcAddress(address);
224
- if (ip === undefined)
225
- return "unknown";
226
- if (ip.family === 4)
227
- return ipv4Scope(ip.octets);
228
- const octets = mappedIpv4(ip.segments);
229
- return octets === undefined ? ipv6Scope(ip.segments) : ipv4Scope(octets);
230
- }
231
- function rtcCandidateEnd(info) {
232
- return {
233
- ...info.type === undefined ? {} : { type: info.type },
234
- ...info.address === undefined ? {} : { address: info.address },
235
- scope: rtcAddressScope(info.address)
236
- };
237
- }
238
- function rtcSelectedPair(local, remote) {
239
- return { local: rtcCandidateEnd(local), remote: rtcCandidateEnd(remote) };
240
- }
241
- function parseRtcAddress(address) {
242
- if (address === undefined)
243
- return;
244
- const text = address.trim();
245
- if (text === "")
246
- return;
247
- let host = text;
248
- if (text.startsWith("[")) {
249
- const close = text.indexOf("]");
250
- if (close === -1)
251
- return;
252
- const tail = text.slice(close + 1);
253
- if (tail !== "" && !/^:\d+$/.test(tail))
254
- return;
255
- host = text.slice(1, close);
256
- } else if (text.split(":").length === 2) {
257
- const [candidate, port] = text.split(":");
258
- if (/^\d+$/.test(port))
259
- host = candidate;
260
- }
261
- const zone = host.indexOf("%");
262
- if (zone !== -1)
263
- host = host.slice(0, zone);
264
- const ipv4 = parseIpv4(host);
265
- if (ipv4 !== undefined)
266
- return { family: 4, octets: ipv4 };
267
- const ipv6 = parseIpv6(host);
268
- return ipv6 === undefined ? undefined : { family: 6, segments: ipv6 };
269
- }
270
- function parseIpv4(text) {
271
- const parts = text.split(".");
272
- if (parts.length !== 4)
273
- return;
274
- const octets = [];
275
- for (const part of parts) {
276
- if (!/^(?:0|[1-9]\d{0,2})$/.test(part))
277
- return;
278
- const value = Number(part);
279
- if (value > 255)
280
- return;
281
- octets.push(value);
282
- }
283
- return octets;
284
- }
285
- function parseIpv6(text) {
286
- const halves = text.split("::");
287
- if (halves.length > 2)
288
- return;
289
- const parsePart = (part, allowIpv4) => {
290
- if (part === "")
291
- return [];
292
- const tokens = part.split(":");
293
- const groups = [];
294
- for (let index = 0;index < tokens.length; index++) {
295
- const token = tokens[index];
296
- if (allowIpv4 && index === tokens.length - 1 && token.includes(".")) {
297
- const v4 = parseIpv4(token);
298
- if (v4 === undefined)
299
- return;
300
- groups.push(v4[0] << 8 | v4[1], v4[2] << 8 | v4[3]);
301
- continue;
302
- }
303
- if (!/^[0-9a-fA-F]{1,4}$/.test(token))
304
- return;
305
- groups.push(Number.parseInt(token, 16));
306
- }
307
- return groups;
308
- };
309
- if (halves.length === 1) {
310
- const groups = parsePart(text, true);
311
- return groups?.length === 8 ? groups : undefined;
312
- }
313
- const left = parsePart(halves[0], false);
314
- const right = parsePart(halves[1], true);
315
- if (left === undefined || right === undefined)
316
- return;
317
- const zeros = 8 - left.length - right.length;
318
- if (zeros < 1)
319
- return;
320
- return [...left, ...new Array(zeros).fill(0), ...right];
321
- }
322
- function mappedIpv4(segments) {
323
- if (!segments.slice(0, 5).every((group) => group === 0) || segments[5] !== 65535)
324
- return;
325
- return [segments[6] >> 8, segments[6] & 255, segments[7] >> 8, segments[7] & 255];
326
- }
327
- function ipv4Scope(octets) {
328
- const [a, b] = octets;
329
- if (a === 127)
330
- return "loopback";
331
- if (a === 100 && b >= 64 && b <= 127)
332
- return "tailnet";
333
- if (a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254) {
334
- return "lan";
335
- }
336
- return "public";
337
- }
338
- function ipv6Scope(segments) {
339
- if (segments[0] === 64890 && segments[1] === 4444 && segments[2] === 41440)
340
- return "tailnet";
341
- if ((segments[0] & 65024) === 64512 || (segments[0] & 65472) === 65152)
342
- return "lan";
343
- if (segments.slice(0, 7).every((group) => group === 0) && segments[7] === 1)
344
- return "loopback";
345
- return "public";
346
- }
347
- var RTC_DEFAULT_STUN = ["stun:stun.cloudflare.com:3478"];
348
- var RTC_DATA_CHANNEL_LABEL = "pinomad";
349
- var RTC_MAX_RECORD_BYTES = 65535;
350
- var RTC_DEFAULT_MAX_MESSAGE_SIZE = 65536;
351
- var RTC_MIN_MAX_MESSAGE_SIZE = RTC_MAX_RECORD_BYTES;
352
- function isRtcHello(value) {
353
- if (typeof value !== "object" || value === null || Array.isArray(value))
354
- return false;
355
- const hello = value;
356
- return Array.isArray(hello.stun) && hello.stun.every((server) => typeof server === "string" && server.length > 0) && hello.signalOnly === true;
357
- }
358
- function rtcCandidateType(candidate) {
359
- const parts = candidate.trim().split(/\s+/);
360
- const at = parts.indexOf("typ");
361
- return at === -1 ? undefined : parts[at + 1];
362
- }
363
- function sdpCandidates(sdp) {
364
- return sdp.split(/\r?\n/).filter((line) => line.startsWith("a=candidate:")).map((line) => line.slice("a=".length));
365
- }
366
- function withRtcCandidates(sdp, candidates) {
367
- const lines = sdp.split(/\r?\n/).filter((line) => line !== "" && !line.startsWith("a=candidate:") && line !== "a=end-of-candidates");
368
- const unique = [...new Set(candidates.map((candidate) => candidate.replace(/^a=/, "")))];
369
- return [...lines, ...unique.map((candidate) => `a=${candidate}`), "a=end-of-candidates", ""].join(`\r
370
- `);
371
- }
372
- function parseRtcMaxMessageSize(sdp) {
373
- const line = sdp.split(/\r?\n/).find((entry) => entry.startsWith("a=max-message-size:"));
374
- if (line === undefined)
375
- return { kind: "bounded", bytes: RTC_DEFAULT_MAX_MESSAGE_SIZE };
376
- const raw = line.slice("a=max-message-size:".length).trim();
377
- if (!/^\d+$/.test(raw))
378
- return { kind: "invalid", reason: `malformed a=max-message-size: ${raw}` };
379
- const bytes = Number(raw);
380
- if (!Number.isSafeInteger(bytes))
381
- return { kind: "invalid", reason: `a=max-message-size is not a safe integer: ${raw}` };
382
- if (bytes === 0)
383
- return { kind: "unlimited" };
384
- if (bytes < RTC_MIN_MAX_MESSAGE_SIZE) {
385
- return { kind: "invalid", reason: `a=max-message-size ${bytes} cannot carry a ${RTC_MIN_MAX_MESSAGE_SIZE}-byte record` };
386
- }
387
- return { kind: "bounded", bytes };
388
- }
389
-
390
390
  // packages/protocol/src/attachments.ts
391
391
  var MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
392
392
  var MAX_ATTACHMENTS_PER_MESSAGE = 10;
@@ -619,4 +619,4 @@ function homeOf(index, summaries, id) {
619
619
  return;
620
620
  }
621
621
 
622
- export { isCustomProviderApi, HOST_STREAM_NAMES, hostStreams, conversationStream, docStream, UNAUTHORIZED_CLOSE_CODE, INVALID_FRAME_CLOSE_CODE, PROTOCOL, hostSupportsCall, hostSupportsFeature, parseClientFrame, rtcSelectedPair, RTC_DEFAULT_STUN, RTC_DATA_CHANNEL_LABEL, RTC_MAX_RECORD_BYTES, isRtcHello, rtcCandidateType, sdpCandidates, withRtcCandidates, parseRtcMaxMessageSize, MAX_ATTACHMENTS_PER_MESSAGE, MAX_PENDING_ATTACHMENTS, MAX_PENDING_ATTACHMENT_BYTES, ATTACHMENT_TTL_MS, MAX_ATTACHMENT_NAME_LENGTH, isAttachmentId, base64DecodedLength, attachmentBeginProblem, attachmentChunkProblem, attachmentBlock, parseAttachmentBlock, organize, checkoutOf, homeOf };
622
+ export { rtcSelectedPair, RTC_DEFAULT_STUN, RTC_DATA_CHANNEL_LABEL, RTC_MAX_RECORD_BYTES, isRtcHello, rtcCandidateType, sdpCandidates, withRtcCandidates, parseRtcMaxMessageSize, isCustomProviderApi, HOST_STREAM_NAMES, hostStreams, conversationStream, docStream, UNAUTHORIZED_CLOSE_CODE, INVALID_FRAME_CLOSE_CODE, PROTOCOL, hostSupportsCall, hostSupportsFeature, parseClientFrame, MAX_ATTACHMENTS_PER_MESSAGE, MAX_PENDING_ATTACHMENTS, MAX_PENDING_ATTACHMENT_BYTES, ATTACHMENT_TTL_MS, MAX_ATTACHMENT_NAME_LENGTH, isAttachmentId, base64DecodedLength, attachmentBeginProblem, attachmentChunkProblem, attachmentBlock, parseAttachmentBlock, organize, checkoutOf, homeOf };
@@ -3,7 +3,7 @@ import {
3
3
  fromBase64Url,
4
4
  parseEndpointSet,
5
5
  toBase64Url
6
- } from "./chunk-sygeb9qb.js";
6
+ } from "./chunk-pqe7q1s7.js";
7
7
 
8
8
  // packages/protocol/src/relay.ts
9
9
  import { ed25519 } from "@noble/curves/ed25519.js";
@@ -12,8 +12,11 @@ var RELAY_CLOSE = {
12
12
  hostNotAllowed: 4603,
13
13
  hostOffline: 4604,
14
14
  unknownConnection: 4605,
15
+ handshakeTimeout: 4608,
15
16
  replaced: 4609,
16
- acceptTimeout: 4610
17
+ acceptTimeout: 4610,
18
+ tooManyConnections: 4629,
19
+ trafficLimit: 4630
17
20
  };
18
21
  function encodeRelayFrame(frame) {
19
22
  return JSON.stringify(frame);
@@ -21,6 +24,7 @@ function encodeRelayFrame(frame) {
21
24
  var isString = (value) => typeof value === "string";
22
25
  var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
23
26
  var isBoundedString = (value, max = 64) => isString(value) && value.length > 0 && value.length <= max;
27
+ var isDeviceKey = (value) => isBoundedString(value) && /^[A-Za-z0-9_-]+$/.test(value);
24
28
  function decodeJson(text) {
25
29
  try {
26
30
  return JSON.parse(text);
@@ -45,6 +49,10 @@ function decodeRelayToHost(text) {
45
49
  if (!isBoundedString(value.requestId))
46
50
  throw new Error("malformed pairing-request");
47
51
  return { t: "pairing-request", requestId: value.requestId };
52
+ case "revoke-device":
53
+ if (!isDeviceKey(value.publicKey))
54
+ throw new Error("malformed revoke-device");
55
+ return { t: "revoke-device", publicKey: value.publicKey };
48
56
  case "push-result":
49
57
  return decodePushResult(value);
50
58
  case "joined":
@@ -140,6 +148,14 @@ function decodeHostToRelay(text) {
140
148
  if (!isBoundedString(value.name, HOST_NAME_MAX_UNITS))
141
149
  throw new Error("malformed name");
142
150
  return { t: "name", name: value.name };
151
+ case "device-admitted":
152
+ if (!isBoundedString(value.requestId) || !isDeviceKey(value.publicKey))
153
+ throw new Error("malformed device-admitted");
154
+ return { t: "device-admitted", requestId: value.requestId, publicKey: value.publicKey };
155
+ case "device-revoked":
156
+ if (!isDeviceKey(value.publicKey))
157
+ throw new Error("malformed device-revoked");
158
+ return { t: "device-revoked", publicKey: value.publicKey };
143
159
  default:
144
160
  throw new Error("unknown host frame");
145
161
  }
@@ -234,6 +250,7 @@ function verifyRelayChallengeV2(signature, args) {
234
250
  // apps/host/src/relay-link.ts
235
251
  import { randomBytes } from "node:crypto";
236
252
  import { WebSocket } from "ws";
253
+ var MAX_UNSENT_REPORTS = 64;
237
254
  function relayWsBase(origin) {
238
255
  const normalized = origin.replace(/\/+$/, "");
239
256
  if (normalized.startsWith("https://"))
@@ -257,6 +274,10 @@ function startRelayLink(options) {
257
274
  const minDelay = options.reconnectDelayMs?.min ?? 1000;
258
275
  const maxDelay = options.reconnectDelayMs?.max ?? 30000;
259
276
  const maxPendingAccepts = options.maxPendingAccepts ?? 16;
277
+ const features = [
278
+ ...options.accountOffer === undefined ? [] : ["pairing"],
279
+ ...options.deviceReports === undefined ? [] : ["device-reports"]
280
+ ];
260
281
  let state = { status: "connecting" };
261
282
  let stopped = false;
262
283
  let control;
@@ -266,6 +287,8 @@ function startRelayLink(options) {
266
287
  let pushEnvironments;
267
288
  let reportedName;
268
289
  let nameDeclared = false;
290
+ let reportsDeclared = false;
291
+ const unsentReports = [];
269
292
  const pushes = new Map;
270
293
  function setState(next) {
271
294
  const changed = state.status !== next.status || state.status === "rejected" && next.status === "rejected" && state.reason !== next.reason;
@@ -302,6 +325,7 @@ function startRelayLink(options) {
302
325
  control = socket;
303
326
  pushEnvironments = undefined;
304
327
  nameDeclared = false;
328
+ reportsDeclared = false;
305
329
  reportedName = undefined;
306
330
  watch(socket);
307
331
  socket.on("message", (data, isBinary) => onControlMessage(socket, data, isBinary));
@@ -336,7 +360,7 @@ function startRelayLink(options) {
336
360
  signature: signRelayChallenge(options.signingKey.secretKey, challenge),
337
361
  signatureV2: signRelayChallengeV2(options.signingKey.secretKey, { ...challenge, noisePublicKey }),
338
362
  noisePublicKey,
339
- ...options.accountOffer === undefined ? {} : { features: ["pairing"] },
363
+ ...features.length === 0 ? {} : { features },
340
364
  ...reportedName === undefined ? {} : { name: reportedName }
341
365
  }));
342
366
  break;
@@ -345,6 +369,12 @@ function startRelayLink(options) {
345
369
  delay = minDelay;
346
370
  pushEnvironments = frame.push;
347
371
  nameDeclared = frame.features?.includes("name") === true;
372
+ reportsDeclared = options.deviceReports !== undefined && frame.features?.includes("device-reports") === true;
373
+ if (reportsDeclared) {
374
+ for (const report of unsentReports.splice(0)) {
375
+ socket.send(encodeRelayFrame({ t: "device-admitted", ...report }));
376
+ }
377
+ }
348
378
  const current = options.hostName?.value;
349
379
  if (nameDeclared && current !== undefined && current !== reportedName) {
350
380
  reportedName = current;
@@ -371,7 +401,7 @@ function startRelayLink(options) {
371
401
  refused("unavailable");
372
402
  break;
373
403
  }
374
- accountOffer().then((result) => {
404
+ accountOffer(frame.requestId).then((result) => {
375
405
  if (socket !== control || socket.readyState !== WebSocket.OPEN)
376
406
  return;
377
407
  const checked = result.ok && result.endpointSet !== undefined ? parseEndpointSet(result.endpointSet) : undefined;
@@ -393,6 +423,18 @@ function startRelayLink(options) {
393
423
  });
394
424
  break;
395
425
  }
426
+ case "revoke-device": {
427
+ const reports = options.deviceReports;
428
+ if (reports === undefined)
429
+ break;
430
+ const { publicKey } = frame;
431
+ reports.revoke(publicKey).then(() => {
432
+ if (socket === control && socket.readyState === WebSocket.OPEN) {
433
+ socket.send(encodeRelayFrame({ t: "device-revoked", publicKey }));
434
+ }
435
+ }, () => {});
436
+ break;
437
+ }
396
438
  case "incoming": {
397
439
  if (pendingAccepts >= maxPendingAccepts)
398
440
  break;
@@ -415,6 +457,7 @@ function startRelayLink(options) {
415
457
  control = undefined;
416
458
  pushEnvironments = undefined;
417
459
  nameDeclared = false;
460
+ reportsDeclared = false;
418
461
  reportedName = undefined;
419
462
  for (const [requestId, request] of pushes) {
420
463
  if (request.socket === socket) {
@@ -478,6 +521,18 @@ function startRelayLink(options) {
478
521
  socket.send(encodeRelayFrame({ t: "push", requestId, ...request }));
479
522
  });
480
523
  },
524
+ reportAdmitted(requestId, publicKey) {
525
+ if (options.deviceReports === undefined)
526
+ return;
527
+ const socket = control;
528
+ if (reportsDeclared && socket !== undefined && socket.readyState === WebSocket.OPEN) {
529
+ socket.send(encodeRelayFrame({ t: "device-admitted", requestId, publicKey }));
530
+ return;
531
+ }
532
+ unsentReports.push({ requestId, publicKey });
533
+ if (unsentReports.length > MAX_UNSENT_REPORTS)
534
+ unsentReports.shift();
535
+ },
481
536
  close() {
482
537
  stopped = true;
483
538
  if (retryTimer !== undefined)
package/dist/pinomad.js CHANGED
@@ -5,39 +5,45 @@ import {
5
5
  } from "./chunk-kzbzacz9.js";
6
6
  import {
7
7
  packagedVersion
8
- } from "./chunk-fqgvf8v0.js";
8
+ } from "./chunk-sd5fkxjj.js";
9
9
  import {
10
10
  __require
11
11
  } from "./chunk-df32avw6.js";
12
12
 
13
13
  // apps/cli/src/pinomad.ts
14
14
  var COMMANDS = {
15
- host: () => import("./chunk-n8cz7skh.js"),
16
- server: () => import("./chunk-cx8jgpdt.js"),
17
- relay: () => import("./chunk-cx8jgpdt.js"),
18
- service: () => import("./chunk-3gcpzpjq.js"),
19
- upgrade: () => import("./chunk-azarj0f4.js"),
20
- link: () => import("./chunk-029mmh7d.js"),
21
- pair: () => import("./chunk-vax4zspw.js"),
22
- join: () => import("./chunk-n58m5hgh.js"),
23
- leave: () => import("./chunk-nj5gz6qh.js")
15
+ up: () => import("./chunk-n1m1tjrk.js"),
16
+ status: () => import("./chunk-b457q0tm.js"),
17
+ host: () => import("./chunk-67gq282t.js"),
18
+ server: () => import("./chunk-9h5vawnm.js"),
19
+ relay: () => import("./chunk-9h5vawnm.js"),
20
+ service: () => import("./chunk-h5q2y57m.js"),
21
+ upgrade: () => import("./chunk-6d16c2fv.js"),
22
+ link: () => import("./chunk-as6pyf00.js"),
23
+ pair: () => import("./chunk-tj57aqav.js"),
24
+ join: () => import("./chunk-greb3dp6.js"),
25
+ leave: () => import("./chunk-4dcs8sbj.js")
24
26
  };
25
27
  var USAGE = `pinomad — PiNomad host, server, and utilities
26
28
 
27
29
  usage: pinomad <command> [args…]
28
30
 
31
+ up [--server] [--lan] [--yes] set this host up: account, LAN, service
32
+ up --origin URL --remote-port N server to join (app.pinomad.si), LAN port (7422)
33
+ up --no-server|--no-lan [--data-dir D] turn a path off; with no flags, up asks
34
+ status [--data-dir D] joined server and account, LAN, service, host
29
35
  host [--data-dir DIR] [--port 7420] [--project DIR]… run the host in the foreground
30
36
  host --remote-port PORT [--public-url URL] remote access, LAN (ADR-0008)
31
37
  host --relay URL remote access via relay (ADR-0015)
32
38
  host --faux "answer" [--faux-tps 40] no real model, for tests
33
39
  server --public-origin O [--data-dir D] [--trust-proxy] run the server
34
- server passwd [--data-dir D] [--user U] change the account password (stdin)
40
+ server passwd [--data-dir D] [--user U] reset an account's password (stdin)
35
41
  relay … old name of server
36
42
  service install|uninstall|status|restart|logs user-level host service
37
43
  upgrade [--force] [address flags] upgrade the package and restart the service
38
44
  link [--data-dir D|--url U …] print the browser link
39
45
  pair [--data-dir D|--url U …] print a pairing QR
40
- join <origin> <token> [--data-dir D] join this host to a server account
46
+ join <origin> [token] [--data-dir D] join this host to a server account
41
47
  leave [--data-dir D] leave the server account
42
48
  --version, -v print the version
43
49
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinomad",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "PiNomad — self-hosted coding agent: host, relay, and web client in one package",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",