wire-mesh 3.4.0 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/server.cjs +23 -9
- package/dist/server.mjs +24 -10
- package/dist/web-console/assets/index-DKnsCgpS.js +72 -0
- package/dist/web-console/index.html +1 -1
- package/dist/web-console/sw.js +1 -1
- package/package.json +3 -3
- package/dist/web-console/assets/index-DQVPlTA5.js +0 -72
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ curl http://<host>:8787/health
|
|
|
56
56
|
|
|
57
57
|
With `--webtransport <host:port>` the node also serves WebTransport on a UDP port, for a browser that has no way to trust a certificate for this node's LAN address. The node mints self-signed ECDSA certificates, each valid for under two weeks, and prints an address of the form `https://<host>:<port>#sha256=<hash>[,<hash>...]` for each address of this machine a client can reach (when bound to a wildcard, one per non-internal IPv4 address). Give one of them to a console (the Node field takes it as typed): a browser accepts the certificate whose hash matches any in the list, with no certificate authority and no internet.
|
|
58
58
|
|
|
59
|
-
The list holds the certificate served now and the ones that will replace it. The node rotates to the next certificate every half lifetime, by replacing its WebTransport server, so open sessions on that server end and a console reconnects, and an address stays usable for about three of those periods after it was copied. Without `--state-dir` the certificates live in memory, so a restart starts a new set and every address already handed out stops working; with it the certificates, private keys included, are kept in that directory (created readable by this user only) and a restart resumes them. The server is the optional `@fails-components/webtransport` dependency with its native binary; where it is not installed the flag fails at startup rather than serving without it. The prebuilt binary needs glibc 2.38 or newer on Linux.
|
|
59
|
+
The list holds the certificate served now and the ones that will replace it. The node rotates to the next certificate every half lifetime, by replacing its WebTransport server, so open sessions on that server end and a console reconnects, and an address stays usable for about three of those periods after it was copied, and a console that reconnects is told the current list by the node, so one that keeps reconnecting never needs a new address. Without `--state-dir` the certificates live in memory, so a restart starts a new set and every address already handed out stops working; with it the certificates, private keys included, are kept in that directory (created readable by this user only) and a restart resumes them. The server is the optional `@fails-components/webtransport` dependency with its native binary; where it is not installed the flag fails at startup rather than serving without it. The prebuilt binary needs glibc 2.38 or newer on Linux, and its install script has to be allowed to run: a package manager that skips install scripts, or holds them for approval as recent npm does (`npm warn allow-scripts`, then `npm approve-scripts`), leaves the binary missing.
|
|
60
60
|
|
|
61
61
|
With `--mailbox-dir` the node also serves the announcer role and `/health` says so (`"roles":["relay","announcer"]`). It then holds other devices' `core/data` logs in that directory while those devices are offline, so a peer that reconnects catches up from the node. Only the device that owns a log can write to it: the node takes a log's entries from the connection whose verified advert names that device and from no one else. Any client that has gossiped an advert can read a log. Entries are opaque bytes to the node, and the applications that write them sign and encrypt their own. The number of logs, the bytes in each and the size of an entry are bounded (`src/mailbox-limits.ts`), and a frame that would exceed a bound is refused whole; nothing is evicted, so a full log stays full until the directory is cleared.
|
|
62
62
|
|
package/dist/server.cjs
CHANGED
|
@@ -261,6 +261,8 @@ async function advanceCertificateSchedule(storage, now, options = {}) {
|
|
|
261
261
|
/** The path a client opens its WebTransport session on. */
|
|
262
262
|
const WEBTRANSPORT_PATH = "/wire-mesh";
|
|
263
263
|
const SECRET_BYTES = 32;
|
|
264
|
+
/** How long to wait before trying a failed rotation again: the port may still be held by the server just stopped. */
|
|
265
|
+
const ROTATION_RETRY_MS = 1e3;
|
|
264
266
|
/** The application close code and reason sent when a certificate rotation ends a session; the code is arbitrary, and 0 is what a normal close carries. */
|
|
265
267
|
const SESSION_CLOSE_CODE = 0;
|
|
266
268
|
const ROTATION_CLOSE_REASON = "certificate rotated";
|
|
@@ -288,7 +290,7 @@ async function loadHttp3Server() {
|
|
|
288
290
|
}
|
|
289
291
|
}
|
|
290
292
|
function isIncomingSession(value) {
|
|
291
|
-
return typeof value === "object" && value !== null && "ready" in value && value.ready instanceof Promise && "closed" in value && value.closed instanceof Promise && "close" in value && typeof value.close === "function" && "incomingBidirectionalStreams" in value && value.incomingBidirectionalStreams instanceof ReadableStream;
|
|
293
|
+
return typeof value === "object" && value !== null && "ready" in value && value.ready instanceof Promise && "closed" in value && value.closed instanceof Promise && "close" in value && typeof value.close === "function" && "createUnidirectionalStream" in value && typeof value.createUnidirectionalStream === "function" && "incomingBidirectionalStreams" in value && value.incomingBidirectionalStreams instanceof ReadableStream;
|
|
292
294
|
}
|
|
293
295
|
/**
|
|
294
296
|
* A WebTransport Transport: each session carries one connection, on the first bidirectional stream the client opens, framed as length-prefixed CBOR (core's byte-stream-connection). A server binds a UDP port and serves a self-signed ECDSA certificate it renews on a schedule, which a browser accepts by pinning its hash (`serverCertificateHashes`) with no certificate authority involved.
|
|
@@ -346,6 +348,9 @@ function createWebTransportTransport(options = {}) {
|
|
|
346
348
|
session.closed.catch(() => void 0).finally(() => {
|
|
347
349
|
liveSessions.delete(session);
|
|
348
350
|
});
|
|
351
|
+
const announcer = (await session.createUnidirectionalStream()).getWriter();
|
|
352
|
+
await announcer.write((0, wire_mesh_core_domain_pinned_address.encodeCertificateHashes)(schedule.advertised.map((certificate) => certificate.sha256Hex)));
|
|
353
|
+
await announcer.close();
|
|
349
354
|
const reader = session.incomingBidirectionalStreams.getReader();
|
|
350
355
|
const first = await reader.read();
|
|
351
356
|
reader.releaseLock();
|
|
@@ -364,14 +369,19 @@ function createWebTransportTransport(options = {}) {
|
|
|
364
369
|
};
|
|
365
370
|
let running = await startServer();
|
|
366
371
|
const advertise = () => reachableHosts(host).map((reachable) => (0, wire_mesh_core_domain_pinned_address.formatPinnedAddress)(`${reachable}:${String(port)}`, schedule.advertised.map((certificate) => certificate.sha256Hex)));
|
|
372
|
+
/** Drains and stops the running server, if there is one: cleared first, so a rotation that failed after this point is retried by starting a server rather than by retiring one that is already gone. */
|
|
367
373
|
const retire = async () => {
|
|
368
|
-
|
|
369
|
-
running
|
|
370
|
-
|
|
374
|
+
const current = running;
|
|
375
|
+
running = void 0;
|
|
376
|
+
if (current === void 0) return;
|
|
377
|
+
await current.drain();
|
|
378
|
+
current.server.stopServer();
|
|
379
|
+
await current.server.closed;
|
|
371
380
|
};
|
|
372
381
|
let rotation;
|
|
373
382
|
let closed = false;
|
|
374
|
-
const
|
|
383
|
+
const untilRotation = () => Math.max(schedule.rotatesAt.getTime() - now().getTime(), 0);
|
|
384
|
+
const armRotation = (delayMs) => {
|
|
375
385
|
rotation = setTimeout(() => {
|
|
376
386
|
(async () => {
|
|
377
387
|
schedule = await advanceCertificateSchedule(storage, now(), scheduleOptions);
|
|
@@ -379,12 +389,16 @@ function createWebTransportTransport(options = {}) {
|
|
|
379
389
|
if (closed) return;
|
|
380
390
|
running = await startServer();
|
|
381
391
|
options.onCertificateRenewed?.(advertise());
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
392
|
+
})().then(() => {
|
|
393
|
+
if (!closed) armRotation(untilRotation());
|
|
394
|
+
}, (error) => {
|
|
395
|
+
reportError(error);
|
|
396
|
+
if (!closed) armRotation(ROTATION_RETRY_MS);
|
|
397
|
+
});
|
|
398
|
+
}, delayMs);
|
|
385
399
|
rotation.unref();
|
|
386
400
|
};
|
|
387
|
-
armRotation();
|
|
401
|
+
armRotation(untilRotation());
|
|
388
402
|
return {
|
|
389
403
|
get address() {
|
|
390
404
|
return `${host}:${String(port)}`;
|
package/dist/server.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { createNodeFsStorage } from "wire-mesh-core/adapters/node-fs-storage";
|
|
|
11
11
|
import { randomBytes } from "node:crypto";
|
|
12
12
|
import { networkInterfaces } from "node:os";
|
|
13
13
|
import { connectionFromByteStream } from "wire-mesh-core/adapters/byte-stream-connection";
|
|
14
|
-
import { formatPinnedAddress } from "wire-mesh-core/domain/pinned-address";
|
|
14
|
+
import { encodeCertificateHashes, formatPinnedAddress } from "wire-mesh-core/domain/pinned-address";
|
|
15
15
|
import { createMemoryStorage } from "wire-mesh-core/adapters/memory-storage";
|
|
16
16
|
import "reflect-metadata";
|
|
17
17
|
import { BasicConstraintsExtension, ExtendedKeyUsage, ExtendedKeyUsageExtension, KeyUsageFlags, KeyUsagesExtension, X509CertificateGenerator } from "@peculiar/x509";
|
|
@@ -260,6 +260,8 @@ async function advanceCertificateSchedule(storage, now, options = {}) {
|
|
|
260
260
|
/** The path a client opens its WebTransport session on. */
|
|
261
261
|
const WEBTRANSPORT_PATH = "/wire-mesh";
|
|
262
262
|
const SECRET_BYTES = 32;
|
|
263
|
+
/** How long to wait before trying a failed rotation again: the port may still be held by the server just stopped. */
|
|
264
|
+
const ROTATION_RETRY_MS = 1e3;
|
|
263
265
|
/** The application close code and reason sent when a certificate rotation ends a session; the code is arbitrary, and 0 is what a normal close carries. */
|
|
264
266
|
const SESSION_CLOSE_CODE = 0;
|
|
265
267
|
const ROTATION_CLOSE_REASON = "certificate rotated";
|
|
@@ -287,7 +289,7 @@ async function loadHttp3Server() {
|
|
|
287
289
|
}
|
|
288
290
|
}
|
|
289
291
|
function isIncomingSession(value) {
|
|
290
|
-
return typeof value === "object" && value !== null && "ready" in value && value.ready instanceof Promise && "closed" in value && value.closed instanceof Promise && "close" in value && typeof value.close === "function" && "incomingBidirectionalStreams" in value && value.incomingBidirectionalStreams instanceof ReadableStream;
|
|
292
|
+
return typeof value === "object" && value !== null && "ready" in value && value.ready instanceof Promise && "closed" in value && value.closed instanceof Promise && "close" in value && typeof value.close === "function" && "createUnidirectionalStream" in value && typeof value.createUnidirectionalStream === "function" && "incomingBidirectionalStreams" in value && value.incomingBidirectionalStreams instanceof ReadableStream;
|
|
291
293
|
}
|
|
292
294
|
/**
|
|
293
295
|
* A WebTransport Transport: each session carries one connection, on the first bidirectional stream the client opens, framed as length-prefixed CBOR (core's byte-stream-connection). A server binds a UDP port and serves a self-signed ECDSA certificate it renews on a schedule, which a browser accepts by pinning its hash (`serverCertificateHashes`) with no certificate authority involved.
|
|
@@ -345,6 +347,9 @@ function createWebTransportTransport(options = {}) {
|
|
|
345
347
|
session.closed.catch(() => void 0).finally(() => {
|
|
346
348
|
liveSessions.delete(session);
|
|
347
349
|
});
|
|
350
|
+
const announcer = (await session.createUnidirectionalStream()).getWriter();
|
|
351
|
+
await announcer.write(encodeCertificateHashes(schedule.advertised.map((certificate) => certificate.sha256Hex)));
|
|
352
|
+
await announcer.close();
|
|
348
353
|
const reader = session.incomingBidirectionalStreams.getReader();
|
|
349
354
|
const first = await reader.read();
|
|
350
355
|
reader.releaseLock();
|
|
@@ -363,14 +368,19 @@ function createWebTransportTransport(options = {}) {
|
|
|
363
368
|
};
|
|
364
369
|
let running = await startServer();
|
|
365
370
|
const advertise = () => reachableHosts(host).map((reachable) => formatPinnedAddress(`${reachable}:${String(port)}`, schedule.advertised.map((certificate) => certificate.sha256Hex)));
|
|
371
|
+
/** Drains and stops the running server, if there is one: cleared first, so a rotation that failed after this point is retried by starting a server rather than by retiring one that is already gone. */
|
|
366
372
|
const retire = async () => {
|
|
367
|
-
|
|
368
|
-
running
|
|
369
|
-
|
|
373
|
+
const current = running;
|
|
374
|
+
running = void 0;
|
|
375
|
+
if (current === void 0) return;
|
|
376
|
+
await current.drain();
|
|
377
|
+
current.server.stopServer();
|
|
378
|
+
await current.server.closed;
|
|
370
379
|
};
|
|
371
380
|
let rotation;
|
|
372
381
|
let closed = false;
|
|
373
|
-
const
|
|
382
|
+
const untilRotation = () => Math.max(schedule.rotatesAt.getTime() - now().getTime(), 0);
|
|
383
|
+
const armRotation = (delayMs) => {
|
|
374
384
|
rotation = setTimeout(() => {
|
|
375
385
|
(async () => {
|
|
376
386
|
schedule = await advanceCertificateSchedule(storage, now(), scheduleOptions);
|
|
@@ -378,12 +388,16 @@ function createWebTransportTransport(options = {}) {
|
|
|
378
388
|
if (closed) return;
|
|
379
389
|
running = await startServer();
|
|
380
390
|
options.onCertificateRenewed?.(advertise());
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
391
|
+
})().then(() => {
|
|
392
|
+
if (!closed) armRotation(untilRotation());
|
|
393
|
+
}, (error) => {
|
|
394
|
+
reportError(error);
|
|
395
|
+
if (!closed) armRotation(ROTATION_RETRY_MS);
|
|
396
|
+
});
|
|
397
|
+
}, delayMs);
|
|
384
398
|
rotation.unref();
|
|
385
399
|
};
|
|
386
|
-
armRotation();
|
|
400
|
+
armRotation(untilRotation());
|
|
387
401
|
return {
|
|
388
402
|
get address() {
|
|
389
403
|
return `${host}:${String(port)}`;
|