pinomad 0.9.0 → 0.10.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.
@@ -6,7 +6,7 @@ import {
6
6
  relayWsBase,
7
7
  signRelayChallenge,
8
8
  signRelayChallengeV2
9
- } from "./chunk-khcpv54k.js";
9
+ } from "./chunk-cx8k63by.js";
10
10
  import {
11
11
  toBase64Url
12
12
  } from "./chunk-sygeb9qb.js";
@@ -55,6 +55,7 @@ function isSecureServerOrigin(origin) {
55
55
  const loopback = host === "localhost" || host.endsWith(".localhost") || ipv4Loopback || host === "::1" || host === "[::1]";
56
56
  return url.protocol === "https:" || url.protocol === "http:" && loopback;
57
57
  }
58
+ var DEVICE_HOST_STATES = new Set(["registered", "revoking", "revoked"]);
58
59
 
59
60
  // apps/host/src/cli/server-ops.ts
60
61
  function assertSecureServerOrigin(origin) {
@@ -122,7 +123,7 @@ async function runServerOperation(options) {
122
123
  finish(() => reject(new Error(`${origin} ignored the operation — the server is too old for accounts; upgrade it first`)));
123
124
  return;
124
125
  }
125
- if (frame.t === "incoming" || frame.t === "pairing-request" || frame.t === "push-result")
126
+ if (frame.t === "incoming" || frame.t === "pairing-request" || frame.t === "push-result" || frame.t === "revoke-device")
126
127
  return;
127
128
  finish(() => resolve(frame));
128
129
  });
@@ -3,24 +3,24 @@ import {
3
3
  assertSecureServerOrigin,
4
4
  probeServerAccounts,
5
5
  runServerOperation
6
- } from "./chunk-3p1eysm9.js";
6
+ } from "./chunk-4h7pkkzr.js";
7
7
  import {
8
8
  loadHostKey,
9
9
  loadRelayKey,
10
10
  readServerConfig,
11
11
  removeServerConfig
12
- } from "./chunk-5gp9svzs.js";
12
+ } from "./chunk-8gywfw5n.js";
13
13
  import {
14
14
  relayHostId
15
- } from "./chunk-khcpv54k.js";
15
+ } from "./chunk-cx8k63by.js";
16
16
  import {
17
17
  argValue,
18
18
  restartService,
19
19
  serviceManager,
20
20
  withoutRelayArgs
21
- } from "./chunk-76pvsg2d.js";
21
+ } from "./chunk-w2m9xkks.js";
22
22
  import"./chunk-kzbzacz9.js";
23
- import"./chunk-vyvyaavr.js";
23
+ import"./chunk-zehwx009.js";
24
24
  import"./chunk-4sepcfpy.js";
25
25
  import"./chunk-jgd38rna.js";
26
26
  import"./chunk-sygeb9qb.js";
@@ -2,13 +2,13 @@
2
2
  import {
3
3
  restartService,
4
4
  serviceManager
5
- } from "./chunk-76pvsg2d.js";
5
+ } from "./chunk-w2m9xkks.js";
6
6
  import"./chunk-kzbzacz9.js";
7
7
  import {
8
8
  globalPrefixOf,
9
9
  packageDir,
10
10
  packagedVersion
11
- } from "./chunk-vyvyaavr.js";
11
+ } from "./chunk-zehwx009.js";
12
12
  import"./chunk-4sepcfpy.js";
13
13
  import"./chunk-jgd38rna.js";
14
14
  import"./chunk-sygeb9qb.js";
@@ -52,7 +52,7 @@ async function admitDevice(harness, input, context) {
52
52
  if (revocations.revoked.includes(input.publicKey)) {
53
53
  revocations.revoked = revocations.revoked.filter((key) => key !== input.publicKey);
54
54
  }
55
- return { outcome: "admitted", entry };
55
+ return { outcome: "admitted", entry, ...offer.requestId === undefined ? {} : { requestId: offer.requestId } };
56
56
  }, context);
57
57
  }
58
58
  async function loadHostKey(dataDir) {
@@ -117,7 +117,7 @@ function pairingOffers(ttlMs = 5 * 60 * 1000) {
117
117
  };
118
118
  return {
119
119
  create: (source) => mint({ source }),
120
- createAccount: (binding) => mint({ source: "account", binding }),
120
+ createAccount: (binding, requestId) => mint({ source: "account", binding, ...requestId === undefined ? {} : { requestId } }),
121
121
  consume(secret) {
122
122
  prune();
123
123
  const candidate = Buffer.from(secret);
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  main,
4
4
  restartService
5
- } from "./chunk-76pvsg2d.js";
5
+ } from "./chunk-w2m9xkks.js";
6
6
  import"./chunk-kzbzacz9.js";
7
- import"./chunk-vyvyaavr.js";
7
+ import"./chunk-zehwx009.js";
8
8
  import"./chunk-4sepcfpy.js";
9
9
  import"./chunk-jgd38rna.js";
10
10
  import"./chunk-sygeb9qb.js";
@@ -3,7 +3,7 @@ import {
3
3
  assertSecureServerOrigin,
4
4
  probeServerAccounts,
5
5
  runServerOperation
6
- } from "./chunk-3p1eysm9.js";
6
+ } from "./chunk-4h7pkkzr.js";
7
7
  import {
8
8
  constantValue,
9
9
  openHostSettings
@@ -12,22 +12,22 @@ import {
12
12
  loadHostKey,
13
13
  loadRelayKey,
14
14
  writeServerConfig
15
- } from "./chunk-5gp9svzs.js";
15
+ } from "./chunk-8gywfw5n.js";
16
16
  import {
17
17
  relayHostId
18
- } from "./chunk-khcpv54k.js";
18
+ } from "./chunk-cx8k63by.js";
19
19
  import {
20
20
  argValue,
21
21
  installSpec,
22
22
  restartService,
23
23
  serviceManager,
24
24
  withoutRelayArgs
25
- } from "./chunk-76pvsg2d.js";
25
+ } from "./chunk-w2m9xkks.js";
26
26
  import"./chunk-kzbzacz9.js";
27
27
  import {
28
28
  commandHint,
29
29
  hostEntry
30
- } from "./chunk-vyvyaavr.js";
30
+ } from "./chunk-zehwx009.js";
31
31
  import"./chunk-4sepcfpy.js";
32
32
  import"./chunk-jgd38rna.js";
33
33
  import {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  packagedVersion
3
- } from "./chunk-vyvyaavr.js";
3
+ } from "./chunk-zehwx009.js";
4
4
  import {
5
5
  hostAddress
6
6
  } from "./chunk-ngf1zatm.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)
@@ -21,16 +21,16 @@ import {
21
21
  readServerBinding,
22
22
  readServerConfig,
23
23
  resolveRelayOrigin
24
- } from "./chunk-5gp9svzs.js";
24
+ } from "./chunk-8gywfw5n.js";
25
25
  import {
26
26
  relayHostId,
27
27
  relayWsBase,
28
28
  startRelayLink
29
- } from "./chunk-khcpv54k.js";
29
+ } from "./chunk-cx8k63by.js";
30
30
  import {
31
31
  packagedVersion,
32
32
  webRoot
33
- } from "./chunk-vyvyaavr.js";
33
+ } from "./chunk-zehwx009.js";
34
34
  import {
35
35
  ATTACHMENT_TTL_MS,
36
36
  INVALID_FRAME_CLOSE_CODE,
@@ -3355,6 +3355,14 @@ async function startGateway(options) {
3355
3355
  };
3356
3356
  let remote;
3357
3357
  let relay;
3358
+ const closeDevice = (publicKey) => {
3359
+ for (const connection of [...connections]) {
3360
+ if (connection.devicePublicKey === publicKey) {
3361
+ connection.closeConnection(UNAUTHORIZED_CLOSE_CODE, "device revoked");
3362
+ }
3363
+ }
3364
+ rtc?.closeDevice(publicKey);
3365
+ };
3358
3366
  const currentAdvertisement = () => {
3359
3367
  const advertiseUrl = remote?.advertiseUrl;
3360
3368
  const endpoints = remoteEndpointCandidates({
@@ -3381,14 +3389,7 @@ async function startGateway(options) {
3381
3389
  rtc?.closeOwner(self);
3382
3390
  self.dispose();
3383
3391
  },
3384
- closeDevice: (publicKey) => {
3385
- for (const connection2 of [...connections]) {
3386
- if (connection2.devicePublicKey === publicKey) {
3387
- connection2.closeConnection(UNAUTHORIZED_CLOSE_CODE, "device revoked");
3388
- }
3389
- }
3390
- rtc?.closeDevice(publicKey);
3391
- },
3392
+ closeDevice,
3392
3393
  rtcOffer: (owner, key, attempt, sdp) => {
3393
3394
  if (rtc === undefined)
3394
3395
  throw new Error("WebRTC is not available on this host");
@@ -3462,7 +3463,8 @@ async function startGateway(options) {
3462
3463
  acceptAccountDevices: () => options.settings.value.acceptAccountDevices,
3463
3464
  serverBinding: async () => await readServerBinding(options.dataDir).catch(() => {
3464
3465
  return;
3465
- })
3466
+ }),
3467
+ accountAdmitted: (requestId, publicKey) => relay?.reportAdmitted(requestId, publicKey)
3466
3468
  };
3467
3469
  const secure = options.secure;
3468
3470
  const handshakeHandlers = {
@@ -3515,6 +3517,16 @@ async function startGateway(options) {
3515
3517
  });
3516
3518
  if (secure?.relay !== undefined) {
3517
3519
  const relayOrigin = secure.relay.origin;
3520
+ const currentBinding = async () => {
3521
+ const config = await readServerConfig(options.dataDir).catch(() => {
3522
+ return;
3523
+ });
3524
+ if (config?.binding === undefined)
3525
+ return { refused: "not_joined" };
3526
+ if (normalizeOrigin(config.origin) !== normalizeOrigin(relayOrigin))
3527
+ return { refused: "rebinding" };
3528
+ return { binding: config.binding };
3529
+ };
3518
3530
  let link;
3519
3531
  link = startRelayLink({
3520
3532
  origin: relayOrigin,
@@ -3527,17 +3539,12 @@ async function startGateway(options) {
3527
3539
  },
3528
3540
  subscribe: (listener) => options.settings.subscribe((settings) => listener(settings.hostName))
3529
3541
  },
3530
- accountOffer: async () => {
3542
+ accountOffer: async (requestId) => {
3531
3543
  if (!options.settings.value.acceptAccountDevices)
3532
3544
  return { ok: false, reason: "disabled" };
3533
- const config = await readServerConfig(options.dataDir).catch(() => {
3534
- return;
3535
- });
3536
- if (config?.binding === undefined)
3537
- return { ok: false, reason: "not_joined" };
3538
- if (normalizeOrigin(config.origin) !== normalizeOrigin(relayOrigin)) {
3539
- return { ok: false, reason: "rebinding" };
3540
- }
3545
+ const bound = await currentBinding();
3546
+ if ("refused" in bound)
3547
+ return { ok: false, reason: bound.refused };
3541
3548
  let endpointSet;
3542
3549
  try {
3543
3550
  const current = checkedEndpointSet(currentAdvertisement().endpoints);
@@ -3545,9 +3552,18 @@ async function startGateway(options) {
3545
3552
  } catch {
3546
3553
  return { ok: false, reason: "invalid_endpoint" };
3547
3554
  }
3548
- const { secret, expiresAt } = secure.offers.createAccount(config.binding);
3555
+ const { secret, expiresAt } = secure.offers.createAccount(bound.binding, requestId);
3549
3556
  return { ok: true, secret, expiresAt, ...endpointSet === undefined ? {} : { endpointSet } };
3550
3557
  },
3558
+ deviceReports: {
3559
+ revoke: async (publicKey) => {
3560
+ const bound = await currentBinding();
3561
+ if ("refused" in bound)
3562
+ throw new Error(`revocation refused: ${bound.refused}`);
3563
+ await revokeDevice(options.harness, publicKey, context3);
3564
+ closeDevice(publicKey);
3565
+ }
3566
+ },
3551
3567
  onStateChange: (state) => {
3552
3568
  if (state.status === "registered") {
3553
3569
  const push = state.push === undefined ? "undeclared" : state.push.length === 0 ? "none" : state.push.join(",");
@@ -3817,6 +3833,9 @@ function secureHandshake(carrier, secure, policy, handlers) {
3817
3833
  carrier.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
3818
3834
  return;
3819
3835
  }
3836
+ if (decision.outcome === "admitted" && decision.requestId !== undefined) {
3837
+ policy.accountAdmitted(decision.requestId, deviceKey);
3838
+ }
3820
3839
  const { message, result: result2 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
3821
3840
  return {
3822
3841
  result: result2,