pinomad 0.6.0 → 0.8.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 (30) hide show
  1. package/README.md +6 -59
  2. package/dist/{chunk-wqvp1fwh.js → chunk-1r8a2434.js} +38 -15
  3. package/dist/{chunk-ywmm63ks.js → chunk-3p1eysm9.js} +4 -2
  4. package/dist/{chunk-d5ypmmr7.js → chunk-73vxwjs7.js} +2 -2
  5. package/dist/chunk-a4b61tx5.js +2984 -0
  6. package/dist/{chunk-k2s1hfq5.js → chunk-cytnycs9.js} +9 -9
  7. package/dist/{chunk-3dnnaz4j.js → chunk-ddh6wfv2.js} +8 -8
  8. package/dist/{chunk-jn7y815x.js → chunk-fes7wjw1.js} +5 -5
  9. package/dist/chunk-jgd38rna.js +622 -0
  10. package/dist/{chunk-4btj08we.js → chunk-jjtr7fr5.js} +5608 -2777
  11. package/dist/{chunk-7qb9tq54.js → chunk-khcpv54k.js} +64 -310
  12. package/dist/{chunk-7s1f4z4g.js → chunk-mv94ksvb.js} +22 -5
  13. package/dist/{chunk-367n9je8.js → chunk-p15hqqjz.js} +1 -1
  14. package/dist/{chunk-c8nxym2z.js → chunk-p56xf1r8.js} +1 -1
  15. package/dist/{chunk-ggb1taqy.js → chunk-ptjyk94f.js} +5 -5
  16. package/dist/{chunk-0xh6cy9v.js → chunk-r2ssa2ex.js} +1 -1
  17. package/dist/chunk-sygeb9qb.js +430 -0
  18. package/dist/{chunk-b1h374jr.js → chunk-v414b5pp.js} +8 -2
  19. package/dist/pinomad.js +10 -10
  20. package/package.json +2 -1
  21. package/web/assets/index-5DfEA9Zb.js +398 -0
  22. package/web/assets/index-D6qIPhPy.css +1 -0
  23. package/web/index.html +2 -2
  24. package/dist/chunk-d93kvxvc.js +0 -610
  25. package/dist/chunk-n4tjhcb7.js +0 -257
  26. package/dist/chunk-znhn8af7.js +0 -33
  27. package/web/assets/index-C_aU_ZiV.js +0 -383
  28. package/web/assets/index-DaPcXZPM.css +0 -1
  29. package/web/assets/montserrat-latin-ext-wght-normal-BsZE-iaG.woff2 +0 -0
  30. package/web/assets/montserrat-latin-wght-normal-l_AIctKy.woff2 +0 -0
@@ -1,287 +1,12 @@
1
1
  import {
2
- HOST_NAME_MAX_UNITS
3
- } from "./chunk-znhn8af7.js";
2
+ HOST_NAME_MAX_UNITS,
3
+ fromBase64Url,
4
+ parseEndpointSet,
5
+ toBase64Url
6
+ } from "./chunk-sygeb9qb.js";
4
7
 
5
8
  // packages/protocol/src/relay.ts
6
9
  import { ed25519 } from "@noble/curves/ed25519.js";
7
-
8
- // packages/protocol/src/noise.ts
9
- import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
10
- import { x25519 } from "@noble/curves/ed25519.js";
11
- import { blake2s } from "@noble/hashes/blake2.js";
12
- import { hmac } from "@noble/hashes/hmac.js";
13
- var PROTOCOL_NAME = new TextEncoder().encode("Noise_IK_25519_ChaChaPoly_BLAKE2s");
14
- var KEY_LENGTH = 32;
15
- var TAG_LENGTH = 16;
16
- var MAX_MESSAGE = 65535;
17
- var MAX_NONCE = 2n ** 64n - 1n;
18
- var hash = (data) => blake2s(data, { dkLen: KEY_LENGTH });
19
- var concat = (...parts) => {
20
- const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
21
- let offset = 0;
22
- for (const part of parts) {
23
- out.set(part, offset);
24
- offset += part.length;
25
- }
26
- return out;
27
- };
28
- function hkdf(chainingKey, ikm, outputs) {
29
- const tempKey = hmac(blake2s, chainingKey, ikm);
30
- const out1 = hmac(blake2s, tempKey, new Uint8Array([1]));
31
- const out2 = hmac(blake2s, tempKey, concat(out1, new Uint8Array([2])));
32
- return [out1, out2];
33
- }
34
- function generateKeyPair() {
35
- return keyPairFromPrivate(x25519.utils.randomSecretKey());
36
- }
37
- function keyPairFromPrivate(privateKey) {
38
- if (privateKey.length !== KEY_LENGTH)
39
- throw new Error(`X25519 private key must be ${KEY_LENGTH} bytes`);
40
- return { publicKey: x25519.getPublicKey(privateKey), privateKey };
41
- }
42
- var dh = (pair, remotePublic) => x25519.getSharedSecret(pair.privateKey, remotePublic);
43
-
44
- class NoiseCipherState {
45
- #key;
46
- #nonce = 0n;
47
- setKey(key) {
48
- this.#key = key;
49
- this.#nonce = 0n;
50
- }
51
- encrypt(plaintext, ad = new Uint8Array(0)) {
52
- const key = this.#key;
53
- if (key === undefined)
54
- return plaintext;
55
- return chacha20poly1305(key, this.#takeNonce(), ad).encrypt(plaintext);
56
- }
57
- decrypt(ciphertext, ad = new Uint8Array(0)) {
58
- const key = this.#key;
59
- if (key === undefined)
60
- return ciphertext;
61
- return chacha20poly1305(key, this.#takeNonce(), ad).decrypt(ciphertext);
62
- }
63
- #takeNonce() {
64
- if (this.#nonce === MAX_NONCE)
65
- throw new Error("nonce exhausted");
66
- const nonce = new Uint8Array(12);
67
- new DataView(nonce.buffer).setBigUint64(4, this.#nonce, true);
68
- this.#nonce += 1n;
69
- return nonce;
70
- }
71
- }
72
-
73
- class SymmetricState {
74
- #ck;
75
- #h;
76
- cipher = new NoiseCipherState;
77
- constructor() {
78
- const name = PROTOCOL_NAME;
79
- this.#h = name.length <= KEY_LENGTH ? concat(name, new Uint8Array(KEY_LENGTH - name.length)) : hash(name);
80
- this.#ck = this.#h.slice();
81
- }
82
- mixKey(ikm) {
83
- const [ck, tempKey] = hkdf(this.#ck, ikm, 2);
84
- this.#ck = ck;
85
- this.cipher.setKey(tempKey.subarray(0, KEY_LENGTH));
86
- }
87
- mixHash(data) {
88
- this.#h = hash(concat(this.#h, data));
89
- }
90
- encryptAndHash(plaintext) {
91
- const ciphertext = this.cipher.encrypt(plaintext, this.#h);
92
- this.mixHash(ciphertext);
93
- return ciphertext;
94
- }
95
- decryptAndHash(ciphertext) {
96
- const plaintext = this.cipher.decrypt(ciphertext, this.#h);
97
- this.mixHash(ciphertext);
98
- return plaintext;
99
- }
100
- get handshakeHash() {
101
- return this.#h;
102
- }
103
- split() {
104
- const [key1, key2] = hkdf(this.#ck, new Uint8Array(0), 2);
105
- const first = new NoiseCipherState;
106
- first.setKey(key1.subarray(0, KEY_LENGTH));
107
- const second = new NoiseCipherState;
108
- second.setKey(key2.subarray(0, KEY_LENGTH));
109
- return [first, second];
110
- }
111
- }
112
- function take(message, size) {
113
- if (message.offset + size > message.buffer.length)
114
- throw new Error("malformed Noise message");
115
- const slice = message.buffer.subarray(message.offset, message.offset + size);
116
- message.offset += size;
117
- return slice;
118
- }
119
- function readMessage1(state, message, own) {
120
- const remoteEphemeral = take(message, KEY_LENGTH).slice();
121
- state.mixHash(remoteEphemeral);
122
- state.mixKey(dh(own.static, remoteEphemeral));
123
- const remoteStatic = state.decryptAndHash(take(message, KEY_LENGTH + TAG_LENGTH)).slice();
124
- if (remoteStatic.length !== KEY_LENGTH)
125
- throw new Error("bad remote static key");
126
- state.mixKey(dh(own.static, remoteStatic));
127
- return { remoteEphemeral, remoteStatic };
128
- }
129
- function writeMessage2(state, buffer, own, remoteEphemeral, remoteStatic) {
130
- buffer.push(own.ephemeral.publicKey);
131
- state.mixHash(own.ephemeral.publicKey);
132
- state.mixKey(dh(own.ephemeral, remoteEphemeral));
133
- state.mixKey(dh(own.ephemeral, remoteStatic));
134
- }
135
- var checkLength = (message, what) => {
136
- if (message.length > MAX_MESSAGE)
137
- throw new Error(`${what} exceeds ${MAX_MESSAGE} bytes`);
138
- };
139
- function initialize(prologue, responderStatic) {
140
- const state = new SymmetricState;
141
- state.mixHash(prologue);
142
- state.mixHash(responderStatic);
143
- return state;
144
- }
145
- function respondIK(options) {
146
- const ephemeral = options.ephemeral ?? generateKeyPair();
147
- const state = initialize(options.prologue, options.static.publicKey);
148
- let remoteEphemeral;
149
- let remoteStatic;
150
- return {
151
- readMessage1(message) {
152
- checkLength(message, "handshake message");
153
- const reader = { buffer: message, offset: 0 };
154
- const remote = readMessage1(state, reader, { static: options.static });
155
- remoteEphemeral = remote.remoteEphemeral;
156
- remoteStatic = remote.remoteStatic;
157
- const payload = state.decryptAndHash(take(reader, message.length - reader.offset));
158
- return { payload, remoteStatic };
159
- },
160
- writeMessage2(payload) {
161
- if (remoteEphemeral === undefined || remoteStatic === undefined)
162
- throw new Error("message 2 before message 1");
163
- const buffer = [];
164
- writeMessage2(state, buffer, { ephemeral }, remoteEphemeral, remoteStatic);
165
- buffer.push(state.encryptAndHash(payload));
166
- const message = concat(...buffer);
167
- checkLength(message, "handshake message");
168
- const [receive, send] = state.split();
169
- return { message, result: { send, receive, handshakeHash: state.handshakeHash } };
170
- }
171
- };
172
- }
173
-
174
- // packages/protocol/src/secure-channel.ts
175
- var SECURE_PROLOGUE = new TextEncoder().encode("pinomad/1");
176
- var utf8 = new TextEncoder;
177
- var utf8Decode = new TextDecoder("utf-8", { fatal: true });
178
- var encodeHostHello = (hello) => utf8.encode(JSON.stringify(hello));
179
- function decodeJson(bytes) {
180
- try {
181
- return JSON.parse(utf8Decode.decode(bytes));
182
- } catch {
183
- throw new Error("handshake payload is not JSON");
184
- }
185
- }
186
- var isString = (value) => typeof value === "string";
187
- function decodeClientHello(bytes) {
188
- const value = decodeJson(bytes);
189
- if (value?.v !== 1)
190
- throw new Error("unsupported client hello");
191
- const pair = value.pair;
192
- if (pair === undefined)
193
- return { v: 1 };
194
- const secret = pair.secret;
195
- const name = pair.name;
196
- if (!isString(secret) || !isString(name))
197
- throw new Error("malformed pair");
198
- return { v: 1, pair: { secret, name } };
199
- }
200
- var B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
201
- function toBase64Url(bytes) {
202
- let out = "";
203
- for (let index = 0;index < bytes.length; index += 3) {
204
- const a = bytes[index];
205
- const b = bytes[index + 1];
206
- const c = bytes[index + 2];
207
- out += B64_ALPHABET[a >> 2] + B64_ALPHABET[(a & 3) << 4 | (b ?? 0) >> 4];
208
- if (b !== undefined)
209
- out += B64_ALPHABET[(b & 15) << 2 | (c ?? 0) >> 6];
210
- if (c !== undefined)
211
- out += B64_ALPHABET[c & 63];
212
- }
213
- return out;
214
- }
215
- function fromBase64Url(text) {
216
- if (text.length % 4 === 1)
217
- throw new Error("invalid base64url");
218
- const lookup = new Map(Array.from(B64_ALPHABET, (char, index) => [char, index]));
219
- const bytes = [];
220
- for (let index = 0;index < text.length; index += 4) {
221
- const quad = text.slice(index, index + 4);
222
- const values = [...quad].map((char) => {
223
- const value = lookup.get(char);
224
- if (value === undefined)
225
- throw new Error("invalid base64url");
226
- return value;
227
- });
228
- const packed = values.reduce((bits, value, index2) => bits | value << 18 - index2 * 6, 0);
229
- bytes.push(packed >> 16 & 255);
230
- if (values.length > 2)
231
- bytes.push(packed >> 8 & 255);
232
- if (values.length > 3)
233
- bytes.push(packed & 255);
234
- }
235
- return new Uint8Array(bytes);
236
- }
237
- var MAX_BODY = 65535 - 16 - 1;
238
-
239
- class SecureSession {
240
- #send;
241
- #receive;
242
- #pending = [];
243
- constructor(result) {
244
- this.#send = result.send;
245
- this.#receive = result.receive;
246
- }
247
- seal(text) {
248
- const body = utf8.encode(text);
249
- const chunks = [];
250
- for (let offset = 0;offset <= body.length; offset += MAX_BODY) {
251
- const last = offset + MAX_BODY >= body.length;
252
- const record = new Uint8Array(1 + Math.min(MAX_BODY, body.length - offset));
253
- record[0] = last ? 0 : 1;
254
- record.set(body.subarray(offset, offset + MAX_BODY), 1);
255
- chunks.push(this.#send.encrypt(record));
256
- if (last)
257
- break;
258
- }
259
- return chunks;
260
- }
261
- open(chunk) {
262
- const record = this.#receive.decrypt(chunk);
263
- const flag = record[0];
264
- if (flag !== 0 && flag !== 1)
265
- throw new Error("bad record flag");
266
- this.#pending.push(record.subarray(1));
267
- if (flag === 1)
268
- return;
269
- const whole = concatParts(this.#pending);
270
- this.#pending = [];
271
- return utf8Decode.decode(whole);
272
- }
273
- }
274
- function concatParts(parts) {
275
- const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
276
- let offset = 0;
277
- for (const part of parts) {
278
- out.set(part, offset);
279
- offset += part.length;
280
- }
281
- return out;
282
- }
283
-
284
- // packages/protocol/src/relay.ts
285
10
  var RELAY_CLOSE = {
286
11
  badSignature: 4601,
287
12
  hostNotAllowed: 4603,
@@ -293,10 +18,10 @@ var RELAY_CLOSE = {
293
18
  function encodeRelayFrame(frame) {
294
19
  return JSON.stringify(frame);
295
20
  }
296
- var isString2 = (value) => typeof value === "string";
21
+ var isString = (value) => typeof value === "string";
297
22
  var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
298
- var isBoundedString = (value, max = 64) => isString2(value) && value.length > 0 && value.length <= max;
299
- function decodeJson2(text) {
23
+ var isBoundedString = (value, max = 64) => isString(value) && value.length > 0 && value.length <= max;
24
+ function decodeJson(text) {
300
25
  try {
301
26
  return JSON.parse(text);
302
27
  } catch {
@@ -304,16 +29,16 @@ function decodeJson2(text) {
304
29
  }
305
30
  }
306
31
  function decodeRelayToHost(text) {
307
- const value = decodeJson2(text);
32
+ const value = decodeJson(text);
308
33
  switch (value?.t) {
309
34
  case "challenge":
310
- if (!isString2(value.nonce))
35
+ if (!isString(value.nonce))
311
36
  throw new Error("malformed challenge");
312
37
  return { t: "challenge", nonce: value.nonce };
313
38
  case "registered":
314
39
  return decodeRegistered(value);
315
40
  case "incoming":
316
- if (!isString2(value.connId))
41
+ if (!isString(value.connId))
317
42
  throw new Error("malformed incoming");
318
43
  return { t: "incoming", connId: value.connId };
319
44
  case "pairing-request":
@@ -323,27 +48,27 @@ function decodeRelayToHost(text) {
323
48
  case "push-result":
324
49
  return decodePushResult(value);
325
50
  case "joined":
326
- if (!isString2(value.username))
51
+ if (!isString(value.username))
327
52
  throw new Error("malformed joined");
328
53
  return { t: "joined", username: value.username };
329
54
  case "left":
330
55
  return { t: "left" };
331
56
  case "refused":
332
- if (!isString2(value.reason))
57
+ if (!isString(value.reason))
333
58
  throw new Error("malformed refused");
334
59
  return { t: "refused", reason: value.reason };
335
60
  default:
336
61
  throw new Error("unknown relay frame");
337
62
  }
338
63
  }
339
- var isFeatureList = (value) => Array.isArray(value) && value.length <= 16 && value.every((entry) => isString2(entry) && entry.length <= 64);
64
+ var isFeatureList = (value) => Array.isArray(value) && value.length <= 16 && value.every((entry) => isString(entry) && entry.length <= 64);
340
65
  function decodeRegistered(value) {
341
66
  if (value.features !== undefined && !isFeatureList(value.features))
342
67
  throw new Error("malformed registered");
343
68
  const declared = value.features === undefined ? {} : { features: value.features };
344
69
  if (value.push === undefined)
345
70
  return { t: "registered", ...declared };
346
- if (!Array.isArray(value.push) || value.push.length > 8 || !value.push.every(isString2)) {
71
+ if (!Array.isArray(value.push) || value.push.length > 8 || !value.push.every(isString)) {
347
72
  throw new Error("malformed registered");
348
73
  }
349
74
  const push = value.push.filter((entry) => entry === "production" || entry === "development");
@@ -379,8 +104,17 @@ function decodePushResult(value) {
379
104
  }
380
105
  return { t: "push-result", requestId: value.requestId, result: value.result, reason: value.reason };
381
106
  }
107
+ function decodePairingEndpointSet(value) {
108
+ const parsed = parseEndpointSet(value);
109
+ if (parsed.kind === "absent" || parsed.kind === "unsupported")
110
+ return;
111
+ if (parsed.kind === "malformed" || parsed.endpointSet.endpoints.length === 0) {
112
+ throw new Error("malformed pairing");
113
+ }
114
+ return parsed.endpointSet;
115
+ }
382
116
  function decodeHostToRelay(text) {
383
- const value = decodeJson2(text);
117
+ const value = decodeJson(text);
384
118
  switch (value?.t) {
385
119
  case "register":
386
120
  return decodeRegister(value);
@@ -390,7 +124,13 @@ function decodeHostToRelay(text) {
390
124
  if (!isBoundedString(value.requestId) || !isBoundedString(value.secret) || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt)) {
391
125
  throw new Error("malformed pairing");
392
126
  }
393
- return { t: "pairing", requestId: value.requestId, secret: value.secret, expiresAt: value.expiresAt };
127
+ return {
128
+ t: "pairing",
129
+ requestId: value.requestId,
130
+ secret: value.secret,
131
+ expiresAt: value.expiresAt,
132
+ ...value.endpointSet === undefined ? {} : { endpointSet: decodePairingEndpointSet(value.endpointSet) }
133
+ };
394
134
  case "pairing-refused":
395
135
  if (!isBoundedString(value.requestId) || !isBoundedString(value.reason)) {
396
136
  throw new Error("malformed pairing-refused");
@@ -424,18 +164,18 @@ function decodePush(value) {
424
164
  };
425
165
  }
426
166
  function decodeRegister(value) {
427
- if (value.t !== "register" || !isString2(value.hostId) || !isString2(value.signature)) {
167
+ if (value.t !== "register" || !isString(value.hostId) || !isString(value.signature)) {
428
168
  throw new Error("malformed register");
429
169
  }
430
170
  if (value.op !== undefined && value.op !== "join" && value.op !== "leave")
431
171
  throw new Error("malformed register");
432
- if (value.signatureV2 !== undefined && !isString2(value.signatureV2))
172
+ if (value.signatureV2 !== undefined && !isString(value.signatureV2))
433
173
  throw new Error("malformed register");
434
- if (value.noisePublicKey !== undefined && !isString2(value.noisePublicKey))
174
+ if (value.noisePublicKey !== undefined && !isString(value.noisePublicKey))
435
175
  throw new Error("malformed register");
436
- if (value.joinToken !== undefined && !isString2(value.joinToken))
176
+ if (value.joinToken !== undefined && !isString(value.joinToken))
437
177
  throw new Error("malformed register");
438
- if (value.name !== undefined && !isString2(value.name))
178
+ if (value.name !== undefined && !isString(value.name))
439
179
  throw new Error("malformed register");
440
180
  if (value.features !== undefined && !isFeatureList(value.features))
441
181
  throw new Error("malformed register");
@@ -456,9 +196,9 @@ function relayHostId(publicKey) {
456
196
  throw new Error("Ed25519 public key must be 32 bytes");
457
197
  return toBase64Url(publicKey);
458
198
  }
459
- var utf82 = new TextEncoder;
199
+ var utf8 = new TextEncoder;
460
200
  function relayChallengeMessage(args) {
461
- return utf82.encode(`pinomad-relay/1
201
+ return utf8.encode(`pinomad-relay/1
462
202
  ${args.relayOrigin}
463
203
  ${args.hostId}
464
204
  ${args.nonce}`);
@@ -474,7 +214,7 @@ function verifyRelayChallenge(signature, args) {
474
214
  }
475
215
  }
476
216
  function relayChallengeMessageV2(args) {
477
- return utf82.encode(`pinomad-relay/2
217
+ return utf8.encode(`pinomad-relay/2
478
218
  ${args.relayOrigin}
479
219
  ${args.hostId}
480
220
  ${args.nonce}
@@ -493,7 +233,7 @@ function verifyRelayChallengeV2(signature, args) {
493
233
 
494
234
  // apps/host/src/relay-link.ts
495
235
  import { randomBytes } from "node:crypto";
496
- import { WebSocket as WebSocket2 } from "ws";
236
+ import { WebSocket } from "ws";
497
237
  function relayWsBase(origin) {
498
238
  const normalized = origin.replace(/\/+$/, "");
499
239
  if (normalized.startsWith("https://"))
@@ -510,6 +250,8 @@ var REJECTION_REASONS = {
510
250
  var PUSH_TIMEOUT_MS = 1e4;
511
251
  function startRelayLink(options) {
512
252
  const origin = options.origin.replace(/\/+$/, "");
253
+ if (origin.includes("#"))
254
+ throw new Error(`relay origin must not include a fragment: ${origin}`);
513
255
  const wsBase = relayWsBase(origin);
514
256
  const hostId = relayHostId(options.signingKey.publicKey);
515
257
  const minDelay = options.reconnectDelayMs?.min ?? 1000;
@@ -536,7 +278,7 @@ function startRelayLink(options) {
536
278
  const pingIntervalMs = options.pingIntervalMs ?? 30000;
537
279
  const pingTimer = setInterval(() => {
538
280
  for (const socket of watched) {
539
- if (socket.readyState !== WebSocket2.OPEN)
281
+ if (socket.readyState !== WebSocket.OPEN)
540
282
  continue;
541
283
  if (ponged.get(socket) === true) {
542
284
  ponged.set(socket, false);
@@ -556,7 +298,7 @@ function startRelayLink(options) {
556
298
  function connectControl() {
557
299
  if (stopped)
558
300
  return;
559
- const socket = new WebSocket2(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
301
+ const socket = new WebSocket(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
560
302
  control = socket;
561
303
  pushEnvironments = undefined;
562
304
  nameDeclared = false;
@@ -630,11 +372,23 @@ function startRelayLink(options) {
630
372
  break;
631
373
  }
632
374
  accountOffer().then((result) => {
633
- if (socket !== control || socket.readyState !== WebSocket2.OPEN)
375
+ if (socket !== control || socket.readyState !== WebSocket.OPEN)
376
+ return;
377
+ const checked = result.ok && result.endpointSet !== undefined ? parseEndpointSet(result.endpointSet) : undefined;
378
+ if (result.ok && (checked?.kind === "malformed" || checked?.kind === "valid" && checked.endpointSet.endpoints.length === 0)) {
379
+ refused("invalid_endpoint");
634
380
  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 }));
381
+ }
382
+ const endpointSet = checked?.kind === "valid" ? checked.endpointSet : undefined;
383
+ socket.send(encodeRelayFrame(result.ok ? {
384
+ t: "pairing",
385
+ requestId: frame.requestId,
386
+ secret: result.secret,
387
+ expiresAt: result.expiresAt,
388
+ ...endpointSet === undefined ? {} : { endpointSet }
389
+ } : { t: "pairing-refused", requestId: frame.requestId, reason: result.reason }));
636
390
  }, () => {
637
- if (socket === control && socket.readyState === WebSocket2.OPEN)
391
+ if (socket === control && socket.readyState === WebSocket.OPEN)
638
392
  refused("error");
639
393
  });
640
394
  break;
@@ -643,7 +397,7 @@ function startRelayLink(options) {
643
397
  if (pendingAccepts >= maxPendingAccepts)
644
398
  break;
645
399
  pendingAccepts += 1;
646
- const data2 = new WebSocket2(`${wsBase}/accept/${frame.connId}`, { handshakeTimeout: pingIntervalMs });
400
+ const data2 = new WebSocket(`${wsBase}/accept/${frame.connId}`, { handshakeTimeout: pingIntervalMs });
647
401
  watch(data2);
648
402
  const release = () => {
649
403
  pendingAccepts -= 1;
@@ -686,7 +440,7 @@ function startRelayLink(options) {
686
440
  if (name === undefined || name === reportedName)
687
441
  return;
688
442
  const socket = control;
689
- if (socket === undefined || socket.readyState !== WebSocket2.OPEN || !nameDeclared)
443
+ if (socket === undefined || socket.readyState !== WebSocket.OPEN || !nameDeclared)
690
444
  return;
691
445
  reportedName = name;
692
446
  socket.send(encodeRelayFrame({ t: "name", name }));
@@ -698,7 +452,7 @@ function startRelayLink(options) {
698
452
  state: () => state,
699
453
  sendPush(request) {
700
454
  const socket = control;
701
- if (stopped || socket === undefined || socket.readyState !== WebSocket2.OPEN || state.status !== "registered") {
455
+ if (stopped || socket === undefined || socket.readyState !== WebSocket.OPEN || state.status !== "registered") {
702
456
  return Promise.resolve({ result: "retry", reason: "control disconnected" });
703
457
  }
704
458
  const envs = pushEnvironments;
@@ -737,4 +491,4 @@ function startRelayLink(options) {
737
491
  };
738
492
  }
739
493
 
740
- export { generateKeyPair, keyPairFromPrivate, respondIK, SECURE_PROLOGUE, encodeHostHello, decodeClientHello, toBase64Url, fromBase64Url, SecureSession, RELAY_CLOSE, encodeRelayFrame, decodeRelayToHost, decodeHostToRelay, relayHostId, signRelayChallenge, verifyRelayChallenge, signRelayChallengeV2, verifyRelayChallengeV2, relayWsBase, startRelayLink };
494
+ export { RELAY_CLOSE, encodeRelayFrame, decodeRelayToHost, decodeHostToRelay, relayHostId, signRelayChallenge, verifyRelayChallenge, signRelayChallengeV2, verifyRelayChallengeV2, relayWsBase, startRelayLink };
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  connectRemoteDurable
3
- } from "./chunk-d93kvxvc.js";
4
- import"./chunk-n4tjhcb7.js";
5
- import"./chunk-znhn8af7.js";
3
+ } from "./chunk-a4b61tx5.js";
4
+ import"./chunk-jgd38rna.js";
5
+ import {
6
+ isLoopbackUrl
7
+ } from "./chunk-sygeb9qb.js";
6
8
  import {
7
9
  hostAddress
8
10
  } from "./chunk-ngf1zatm.js";
@@ -10,16 +12,30 @@ import"./chunk-df32avw6.js";
10
12
 
11
13
  // apps/host/src/cli/pair.ts
12
14
  import { renderUnicodeCompact } from "uqr";
15
+ function pairingQr(link) {
16
+ try {
17
+ return renderUnicodeCompact(link);
18
+ } catch (error) {
19
+ if (error instanceof Error && error.message === "Data too long")
20
+ return;
21
+ throw error;
22
+ }
23
+ }
13
24
  async function printPairing({ url, token }) {
14
25
  const remote = await connectRemoteDurable({ url, token, reconnectDelayMs: { min: 200, max: 2000 } });
15
26
  try {
16
27
  const { url: pairing, expiresAt } = await remote.controller.createPairing();
17
- if (new URL(pairing).hostname === "127.0.0.1") {
28
+ if (isLoopbackUrl(pairing)) {
18
29
  console.log(pairing);
19
30
  console.log("This link only works on this machine — give it to the desktop app.");
20
31
  console.log("To pair a phone, restart the host with --remote-port or --relay, then run pair again.");
21
32
  } else {
22
- console.log(renderUnicodeCompact(pairing));
33
+ const qr = pairingQr(pairing);
34
+ if (qr === undefined) {
35
+ console.log("This link is too long for a QR code. Copy it below and paste it into the app instead.");
36
+ } else {
37
+ console.log(qr);
38
+ }
23
39
  console.log(pairing);
24
40
  }
25
41
  console.log(`Expires at ${new Date(expiresAt).toLocaleTimeString()}`);
@@ -40,5 +56,6 @@ if (false)
40
56
  ;
41
57
  export {
42
58
  printPairing,
59
+ pairingQr,
43
60
  main
44
61
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  packagedVersion
3
- } from "./chunk-c8nxym2z.js";
3
+ } from "./chunk-p56xf1r8.js";
4
4
  import {
5
5
  hostAddress
6
6
  } from "./chunk-ngf1zatm.js";
@@ -1,6 +1,6 @@
1
1
  // apps/host/src/distribution.ts
2
2
  import { fileURLToPath } from "node:url";
3
- var packagedVersion = "0.6.0";
3
+ var packagedVersion = "0.8.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}`;
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  main,
4
4
  restartService
5
- } from "./chunk-d5ypmmr7.js";
5
+ } from "./chunk-73vxwjs7.js";
6
6
  import"./chunk-kzbzacz9.js";
7
- import"./chunk-c8nxym2z.js";
8
- import"./chunk-d93kvxvc.js";
9
- import"./chunk-n4tjhcb7.js";
10
- import"./chunk-znhn8af7.js";
7
+ import"./chunk-p56xf1r8.js";
8
+ import"./chunk-a4b61tx5.js";
9
+ import"./chunk-jgd38rna.js";
10
+ import"./chunk-sygeb9qb.js";
11
11
  import"./chunk-ngf1zatm.js";
12
12
  import"./chunk-df32avw6.js";
13
13
  export {
@@ -3,7 +3,7 @@ import {
3
3
  generateKeyPair,
4
4
  keyPairFromPrivate,
5
5
  toBase64Url
6
- } from "./chunk-7qb9tq54.js";
6
+ } from "./chunk-sygeb9qb.js";
7
7
 
8
8
  // apps/host/src/devices.ts
9
9
  import { link, readFile, unlink, writeFile } from "node:fs/promises";