wire-mesh-core 3.3.1 → 3.4.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.
- package/dist/adapters/byte-stream-connection.d.cts +1 -1
- package/dist/adapters/byte-stream-connection.d.mts +1 -1
- package/dist/adapters/tcp-transport.d.cts +1 -1
- package/dist/adapters/tcp-transport.d.mts +1 -1
- package/dist/adapters/tls-transport.d.cts +1 -1
- package/dist/adapters/tls-transport.d.mts +1 -1
- package/dist/domain/bulk.d.cts +1 -1
- package/dist/domain/bulk.d.mts +1 -1
- package/dist/domain/direct-manage-request.d.cts +1 -1
- package/dist/domain/direct-manage-request.d.mts +1 -1
- package/dist/domain/mesh-session.cjs +11 -2
- package/dist/domain/mesh-session.d.cts +1 -1
- package/dist/domain/mesh-session.d.mts +1 -1
- package/dist/domain/mesh-session.mjs +11 -2
- package/dist/domain/pinned-address.cjs +51 -0
- package/dist/domain/pinned-address.d.cts +15 -0
- package/dist/domain/pinned-address.d.mts +15 -0
- package/dist/domain/pinned-address.mjs +47 -1
- package/dist/domain/relay-hub.d.cts +1 -1
- package/dist/domain/relay-hub.d.mts +1 -1
- package/dist/domain/relay-use-gate.d.cts +1 -1
- package/dist/domain/relay-use-gate.d.mts +1 -1
- package/dist/ports/transport.d.cts +1 -1
- package/dist/ports/transport.d.mts +1 -1
- package/dist/{transport-iLBxIkRS.d.mts → transport-B0GywEzG.d.mts} +2 -0
- package/dist/{transport-CfW7fsGK.d.cts → transport-DIXjoQKn.d.cts} +2 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { N as Frame } from "../protocol-DCDae3z4.cjs";
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
2
|
+
import { t as Connection } from "../transport-DIXjoQKn.cjs";
|
|
3
3
|
//#region src/adapters/byte-stream-connection.d.ts
|
|
4
4
|
/** The largest frame body a peer may announce in a length prefix, matching the 100 MiB the `ws` package applies to one message on the same hub, so a WebSocket peer and a byte-stream peer are bounded alike. Without a bound a peer-controlled 4 GiB prefix makes the reader buffer until memory runs out. */
|
|
5
5
|
export declare const MAX_FRAME_BYTES: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { N as Frame } from "../protocol-DCDae3z4.mjs";
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
2
|
+
import { t as Connection } from "../transport-B0GywEzG.mjs";
|
|
3
3
|
//#region src/adapters/byte-stream-connection.d.ts
|
|
4
4
|
/** The largest frame body a peer may announce in a length prefix, matching the 100 MiB the `ws` package applies to one message on the same hub, so a WebSocket peer and a byte-stream peer are bounded alike. Without a bound a peer-controlled 4 GiB prefix makes the reader buffer until memory runs out. */
|
|
5
5
|
export declare const MAX_FRAME_BYTES: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as Transport } from "../transport-
|
|
1
|
+
import { r as Transport } from "../transport-DIXjoQKn.cjs";
|
|
2
2
|
//#region src/adapters/tcp-transport.d.ts
|
|
3
3
|
/** A Node net.Socket-based Transport: length-prefixed, CBOR-encoded frames over plain TCP -- matching Cascade's own transport shape, since interop with Cascade nodes is wire-mesh's stated goal. Framing (not TLS) is this adapter's own concern; a TLS-terminated variant is a separate adapter behind the same Transport contract. */
|
|
4
4
|
export declare function createTcpTransport(): Transport;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as Transport } from "../transport-
|
|
1
|
+
import { r as Transport } from "../transport-B0GywEzG.mjs";
|
|
2
2
|
//#region src/adapters/tcp-transport.d.ts
|
|
3
3
|
/** A Node net.Socket-based Transport: length-prefixed, CBOR-encoded frames over plain TCP -- matching Cascade's own transport shape, since interop with Cascade nodes is wire-mesh's stated goal. Framing (not TLS) is this adapter's own concern; a TLS-terminated variant is a separate adapter behind the same Transport contract. */
|
|
4
4
|
export declare function createTcpTransport(): Transport;
|
package/dist/domain/bulk.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DCDae3z4.cjs";
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
2
|
+
import { t as Connection } from "../transport-DIXjoQKn.cjs";
|
|
3
3
|
import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
|
|
4
4
|
import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
|
|
5
5
|
//#region src/domain/bulk.d.ts
|
package/dist/domain/bulk.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DCDae3z4.mjs";
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
2
|
+
import { t as Connection } from "../transport-B0GywEzG.mjs";
|
|
3
3
|
import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
|
|
4
4
|
import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
|
|
5
5
|
//#region src/domain/bulk.d.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DCDae3z4.cjs";
|
|
2
|
-
import { r as Transport, t as Connection } from "../transport-
|
|
2
|
+
import { r as Transport, t as Connection } from "../transport-DIXjoQKn.cjs";
|
|
3
3
|
import { IncomingManageRequest, ManageOutcome } from "./mesh-session.cjs";
|
|
4
4
|
//#region src/domain/direct-manage-request.d.ts
|
|
5
5
|
/** Reads exactly one frame off a freshly-accepted, bare connection and, if it's a manage-request, returns an IncomingManageRequest ready to hand to the same application-level dispatch logic session.incomingManageRequests already feeds elsewhere -- no handshake or gossip is ever read or sent on this connection (wire-mesh#38: the manage-request/manage-response exchange has no dependency on handshake state at the dispatch layer, confirmed against mesh-session.ts's own applyManageRequest/applyFrame). respond() sends the manage-response directly and closes the connection, since a one-off request/response is this connection's entire purpose -- unlike a real MeshSession, there is nothing further to do with it afterwards. Resolves null, without closing the connection (that's the caller's call), for any other first frame or if the connection ends before one arrives: interpreting either case is a Transport.listen() caller's own business, e.g. peeking the first frame to route between this path and acceptMeshSession's own handshake path. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DCDae3z4.mjs";
|
|
2
|
-
import { r as Transport, t as Connection } from "../transport-
|
|
2
|
+
import { r as Transport, t as Connection } from "../transport-B0GywEzG.mjs";
|
|
3
3
|
import { IncomingManageRequest, ManageOutcome } from "./mesh-session.mjs";
|
|
4
4
|
//#region src/domain/direct-manage-request.d.ts
|
|
5
5
|
/** Reads exactly one frame off a freshly-accepted, bare connection and, if it's a manage-request, returns an IncomingManageRequest ready to hand to the same application-level dispatch logic session.incomingManageRequests already feeds elsewhere -- no handshake or gossip is ever read or sent on this connection (wire-mesh#38: the manage-request/manage-response exchange has no dependency on handshake state at the dispatch layer, confirmed against mesh-session.ts's own applyManageRequest/applyFrame). respond() sends the manage-response directly and closes the connection, since a one-off request/response is this connection's entire purpose -- unlike a real MeshSession, there is nothing further to do with it afterwards. Resolves null, without closing the connection (that's the caller's call), for any other first frame or if the connection ends before one arrives: interpreting either case is a Transport.listen() caller's own business, e.g. peeking the first frame to route between this path and acceptMeshSession's own handshake path. */
|
|
@@ -504,6 +504,10 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
504
504
|
handshake
|
|
505
505
|
};
|
|
506
506
|
}
|
|
507
|
+
/** The address to dial after `link` ended: the one the adapter says reaches the same peer now, if it says anything, else the one this session dialled. */
|
|
508
|
+
function redialAddress(link, dialled) {
|
|
509
|
+
return link.redialAddress?.() ?? dialled;
|
|
510
|
+
}
|
|
507
511
|
function handleDisconnect(reason, address, localDomains) {
|
|
508
512
|
if (feedCancelled) return;
|
|
509
513
|
rejectPendingManageRequests("disconnected before a response arrived");
|
|
@@ -534,14 +538,19 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
534
538
|
emit();
|
|
535
539
|
}
|
|
536
540
|
async function consume(link, address, localDomains) {
|
|
541
|
+
let heardFromPeer = false;
|
|
537
542
|
for await (const frame of link.receive()) {
|
|
538
543
|
if (feedCancelled) return;
|
|
544
|
+
if (!heardFromPeer) {
|
|
545
|
+
heardFromPeer = true;
|
|
546
|
+
attempt = 0;
|
|
547
|
+
}
|
|
539
548
|
await applyFrame(frame);
|
|
540
549
|
await onFrame?.(link, frame);
|
|
541
550
|
emit();
|
|
542
551
|
}
|
|
543
552
|
onSessionEnd?.(link);
|
|
544
|
-
if (state.status === "connected") handleDisconnect("node closed the connection", address, localDomains);
|
|
553
|
+
if (state.status === "connected") handleDisconnect("node closed the connection", redialAddress(link, address), localDomains);
|
|
545
554
|
}
|
|
546
555
|
/** Builds this side's own self-advert and signs it with this node's own key (wire-mesh#225), which is what makes it usable by a receiver that learned of it through a hub or a gateway rather than directly from here. this node's own directly-reachable addresses (wire-mesh#38) are advertised, or none for a caller with nothing to offer (a browser client, which cannot accept inbound connections) -- either is an honest advert, not a stopgap. extensions merge onto peer-advert's own open `* tstr => any` tail -- the mechanism sendGossipUpdate uses to keep a gossiped fact (presence status, an accept/refuse policy, or any future domain's own) live over the connection's lifetime, and every such key is covered by the signature. Extensions, CORE_VERSION_GOSSIP_KEY, and topology/peers are all spread before the mandatory fields (never after) so a caller-supplied key of the same name can never shadow them on the wire -- validateGossipExtensions already rejects every such collision loudly, but the field order is kept safe in its own right rather than relying solely on the guard staying in sync. topology/peers (wire-mesh#180) is recomputed fresh on every call, the same "always current, never cached" treatment snapshot-seconds already gets, since a session's own connection identity and relay pairings can change between one self-advert and the next. */
|
|
547
556
|
async function buildSelfAdvert(extensions) {
|
|
@@ -596,7 +605,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
596
605
|
}
|
|
597
606
|
}, HANDSHAKE_TIMEOUT_MS);
|
|
598
607
|
consume(connection, address, localDomains).catch((error) => {
|
|
599
|
-
if (state.status === "connected") handleDisconnect(error instanceof Error ? error.message : String(error), address, localDomains);
|
|
608
|
+
if (state.status === "connected") handleDisconnect(error instanceof Error ? error.message : String(error), redialAddress(link, address), localDomains);
|
|
600
609
|
});
|
|
601
610
|
}
|
|
602
611
|
async function doConnect(address, localDomains) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DCDae3z4.cjs";
|
|
2
|
-
import { r as Transport, t as Connection } from "../transport-
|
|
2
|
+
import { r as Transport, t as Connection } from "../transport-DIXjoQKn.cjs";
|
|
3
3
|
import { t as IdentityPort } from "../identity-DVCNuFLG.cjs";
|
|
4
4
|
import { t as Clock } from "../clock-DiSx-WKM.cjs";
|
|
5
5
|
//#region src/domain/mesh-session.d.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DCDae3z4.mjs";
|
|
2
|
-
import { r as Transport, t as Connection } from "../transport-
|
|
2
|
+
import { r as Transport, t as Connection } from "../transport-B0GywEzG.mjs";
|
|
3
3
|
import { t as IdentityPort } from "../identity-DcaYgjXT.mjs";
|
|
4
4
|
import { t as Clock } from "../clock-DiSx-WKM.mjs";
|
|
5
5
|
//#region src/domain/mesh-session.d.ts
|
|
@@ -503,6 +503,10 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
503
503
|
handshake
|
|
504
504
|
};
|
|
505
505
|
}
|
|
506
|
+
/** The address to dial after `link` ended: the one the adapter says reaches the same peer now, if it says anything, else the one this session dialled. */
|
|
507
|
+
function redialAddress(link, dialled) {
|
|
508
|
+
return link.redialAddress?.() ?? dialled;
|
|
509
|
+
}
|
|
506
510
|
function handleDisconnect(reason, address, localDomains) {
|
|
507
511
|
if (feedCancelled) return;
|
|
508
512
|
rejectPendingManageRequests("disconnected before a response arrived");
|
|
@@ -533,14 +537,19 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
533
537
|
emit();
|
|
534
538
|
}
|
|
535
539
|
async function consume(link, address, localDomains) {
|
|
540
|
+
let heardFromPeer = false;
|
|
536
541
|
for await (const frame of link.receive()) {
|
|
537
542
|
if (feedCancelled) return;
|
|
543
|
+
if (!heardFromPeer) {
|
|
544
|
+
heardFromPeer = true;
|
|
545
|
+
attempt = 0;
|
|
546
|
+
}
|
|
538
547
|
await applyFrame(frame);
|
|
539
548
|
await onFrame?.(link, frame);
|
|
540
549
|
emit();
|
|
541
550
|
}
|
|
542
551
|
onSessionEnd?.(link);
|
|
543
|
-
if (state.status === "connected") handleDisconnect("node closed the connection", address, localDomains);
|
|
552
|
+
if (state.status === "connected") handleDisconnect("node closed the connection", redialAddress(link, address), localDomains);
|
|
544
553
|
}
|
|
545
554
|
/** Builds this side's own self-advert and signs it with this node's own key (wire-mesh#225), which is what makes it usable by a receiver that learned of it through a hub or a gateway rather than directly from here. this node's own directly-reachable addresses (wire-mesh#38) are advertised, or none for a caller with nothing to offer (a browser client, which cannot accept inbound connections) -- either is an honest advert, not a stopgap. extensions merge onto peer-advert's own open `* tstr => any` tail -- the mechanism sendGossipUpdate uses to keep a gossiped fact (presence status, an accept/refuse policy, or any future domain's own) live over the connection's lifetime, and every such key is covered by the signature. Extensions, CORE_VERSION_GOSSIP_KEY, and topology/peers are all spread before the mandatory fields (never after) so a caller-supplied key of the same name can never shadow them on the wire -- validateGossipExtensions already rejects every such collision loudly, but the field order is kept safe in its own right rather than relying solely on the guard staying in sync. topology/peers (wire-mesh#180) is recomputed fresh on every call, the same "always current, never cached" treatment snapshot-seconds already gets, since a session's own connection identity and relay pairings can change between one self-advert and the next. */
|
|
546
555
|
async function buildSelfAdvert(extensions) {
|
|
@@ -595,7 +604,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
595
604
|
}
|
|
596
605
|
}, HANDSHAKE_TIMEOUT_MS);
|
|
597
606
|
consume(connection, address, localDomains).catch((error) => {
|
|
598
|
-
if (state.status === "connected") handleDisconnect(error instanceof Error ? error.message : String(error), address, localDomains);
|
|
607
|
+
if (state.status === "connected") handleDisconnect(error instanceof Error ? error.message : String(error), redialAddress(link, address), localDomains);
|
|
599
608
|
});
|
|
600
609
|
}
|
|
601
610
|
async function doConnect(address, localDomains) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let cbor2 = require("cbor2");
|
|
2
3
|
//#region src/domain/pinned-address.ts
|
|
3
4
|
const PIN_FRAGMENT = "#sha256=";
|
|
4
5
|
const HASH_SEPARATOR = ",";
|
|
5
6
|
const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
7
|
+
const HEX_RADIX = 16;
|
|
6
8
|
function hexToBytes(hex) {
|
|
7
9
|
const bytes = new Uint8Array(hex.length / 2);
|
|
8
10
|
for (let index = 0; index < bytes.length; index++) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
@@ -34,7 +36,56 @@ function parsePinnedAddress(address) {
|
|
|
34
36
|
sha256: hashes.map(hexToBytes)
|
|
35
37
|
};
|
|
36
38
|
}
|
|
39
|
+
/** The bytes in a SHA-256 hash. */
|
|
40
|
+
const SHA256_BYTES = 32;
|
|
41
|
+
/** The most hashes a node may announce at once. A browser is asked to accept any of them, and a longer list is a node misbehaving rather than one advertising a schedule. */
|
|
42
|
+
const MAX_ANNOUNCED_HASHES = 8;
|
|
43
|
+
/**
|
|
44
|
+
* The message a node sends a client, on a stream it opens to that client, to say which certificates it serves now and will serve next: a CBOR map with one key, `sha256`, holding an array of 32-byte hashes in the order they take over. It is authentic because it arrives on the session the client pinned, so no signature is needed.
|
|
45
|
+
*/
|
|
46
|
+
function encodeCertificateHashes(sha256Hex) {
|
|
47
|
+
return (0, cbor2.encode)({ sha256: sha256Hex.map(hexToBytes) }, cbor2.cdeEncodeOptions);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reads a message made by `encodeCertificateHashes`.
|
|
51
|
+
* @throws Error when it is not a map with a `sha256` array of one to MAX_ANNOUNCED_HASHES byte strings of 32 bytes each.
|
|
52
|
+
*/
|
|
53
|
+
function decodeCertificateHashes(bytes) {
|
|
54
|
+
const decoded = (0, cbor2.decode)(bytes, cbor2.cdeDecodeOptions);
|
|
55
|
+
if (typeof decoded !== "object" || decoded === null || !("sha256" in decoded) || !Array.isArray(decoded.sha256)) throw new Error("expected a map with a sha256 array");
|
|
56
|
+
const announced = decoded.sha256;
|
|
57
|
+
if (announced.length === 0 || announced.length > 8) throw new Error(`expected 1 to ${String(8)} hashes, got ${String(announced.length)}`);
|
|
58
|
+
const hashes = [];
|
|
59
|
+
for (const hash of announced) {
|
|
60
|
+
if (!(hash instanceof Uint8Array) || hash.length !== SHA256_BYTES) throw new Error(`each hash must be ${String(SHA256_BYTES)} bytes`);
|
|
61
|
+
hashes.push(new Uint8Array(hash));
|
|
62
|
+
}
|
|
63
|
+
return hashes;
|
|
64
|
+
}
|
|
65
|
+
/** `hashes` followed by those of `more` that are not already in it, so a client can pin what it was given and what it has learned since. */
|
|
66
|
+
function mergeHashes(hashes, more) {
|
|
67
|
+
const known = new Set(hashes.map(bytesToHex));
|
|
68
|
+
const merged = [...hashes];
|
|
69
|
+
for (const hash of more) if (!known.has(bytesToHex(hash))) {
|
|
70
|
+
known.add(bytesToHex(hash));
|
|
71
|
+
merged.push(hash);
|
|
72
|
+
}
|
|
73
|
+
return merged;
|
|
74
|
+
}
|
|
75
|
+
function bytesToHex(bytes) {
|
|
76
|
+
return Array.from(bytes, (byte) => byte.toString(HEX_RADIX).padStart(2, "0")).join("");
|
|
77
|
+
}
|
|
78
|
+
/** `address` with its pinned hashes replaced by `sha256`. */
|
|
79
|
+
function withPinnedHashes(address, sha256) {
|
|
80
|
+
const { url } = parsePinnedAddress(address);
|
|
81
|
+
return formatPinnedAddress(new URL(url).host, sha256.map(bytesToHex));
|
|
82
|
+
}
|
|
37
83
|
//#endregion
|
|
84
|
+
exports.MAX_ANNOUNCED_HASHES = MAX_ANNOUNCED_HASHES;
|
|
85
|
+
exports.decodeCertificateHashes = decodeCertificateHashes;
|
|
86
|
+
exports.encodeCertificateHashes = encodeCertificateHashes;
|
|
38
87
|
exports.formatPinnedAddress = formatPinnedAddress;
|
|
39
88
|
exports.isPinnedAddress = isPinnedAddress;
|
|
89
|
+
exports.mergeHashes = mergeHashes;
|
|
40
90
|
exports.parsePinnedAddress = parsePinnedAddress;
|
|
91
|
+
exports.withPinnedHashes = withPinnedHashes;
|
|
@@ -19,4 +19,19 @@ export declare function isPinnedAddress(address: string): boolean;
|
|
|
19
19
|
* @throws Error when the address is not `https://` or does not carry one or more 64-digit lowercase hex hashes.
|
|
20
20
|
*/
|
|
21
21
|
export declare function parsePinnedAddress(address: string): PinnedAddress;
|
|
22
|
+
/** The most hashes a node may announce at once. A browser is asked to accept any of them, and a longer list is a node misbehaving rather than one advertising a schedule. */
|
|
23
|
+
export declare const MAX_ANNOUNCED_HASHES = 8;
|
|
24
|
+
/**
|
|
25
|
+
* The message a node sends a client, on a stream it opens to that client, to say which certificates it serves now and will serve next: a CBOR map with one key, `sha256`, holding an array of 32-byte hashes in the order they take over. It is authentic because it arrives on the session the client pinned, so no signature is needed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function encodeCertificateHashes(sha256Hex: readonly string[]): Uint8Array;
|
|
28
|
+
/**
|
|
29
|
+
* Reads a message made by `encodeCertificateHashes`.
|
|
30
|
+
* @throws Error when it is not a map with a `sha256` array of one to MAX_ANNOUNCED_HASHES byte strings of 32 bytes each.
|
|
31
|
+
*/
|
|
32
|
+
export declare function decodeCertificateHashes(bytes: Readonly<Uint8Array>): Uint8Array<ArrayBuffer>[];
|
|
33
|
+
/** `hashes` followed by those of `more` that are not already in it, so a client can pin what it was given and what it has learned since. */
|
|
34
|
+
export declare function mergeHashes(hashes: readonly Uint8Array<ArrayBuffer>[], more: readonly Uint8Array<ArrayBuffer>[]): Uint8Array<ArrayBuffer>[];
|
|
35
|
+
/** `address` with its pinned hashes replaced by `sha256`. */
|
|
36
|
+
export declare function withPinnedHashes(address: string, sha256: readonly Uint8Array<ArrayBuffer>[]): string;
|
|
22
37
|
//#endregion
|
|
@@ -19,4 +19,19 @@ export declare function isPinnedAddress(address: string): boolean;
|
|
|
19
19
|
* @throws Error when the address is not `https://` or does not carry one or more 64-digit lowercase hex hashes.
|
|
20
20
|
*/
|
|
21
21
|
export declare function parsePinnedAddress(address: string): PinnedAddress;
|
|
22
|
+
/** The most hashes a node may announce at once. A browser is asked to accept any of them, and a longer list is a node misbehaving rather than one advertising a schedule. */
|
|
23
|
+
export declare const MAX_ANNOUNCED_HASHES = 8;
|
|
24
|
+
/**
|
|
25
|
+
* The message a node sends a client, on a stream it opens to that client, to say which certificates it serves now and will serve next: a CBOR map with one key, `sha256`, holding an array of 32-byte hashes in the order they take over. It is authentic because it arrives on the session the client pinned, so no signature is needed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function encodeCertificateHashes(sha256Hex: readonly string[]): Uint8Array;
|
|
28
|
+
/**
|
|
29
|
+
* Reads a message made by `encodeCertificateHashes`.
|
|
30
|
+
* @throws Error when it is not a map with a `sha256` array of one to MAX_ANNOUNCED_HASHES byte strings of 32 bytes each.
|
|
31
|
+
*/
|
|
32
|
+
export declare function decodeCertificateHashes(bytes: Readonly<Uint8Array>): Uint8Array<ArrayBuffer>[];
|
|
33
|
+
/** `hashes` followed by those of `more` that are not already in it, so a client can pin what it was given and what it has learned since. */
|
|
34
|
+
export declare function mergeHashes(hashes: readonly Uint8Array<ArrayBuffer>[], more: readonly Uint8Array<ArrayBuffer>[]): Uint8Array<ArrayBuffer>[];
|
|
35
|
+
/** `address` with its pinned hashes replaced by `sha256`. */
|
|
36
|
+
export declare function withPinnedHashes(address: string, sha256: readonly Uint8Array<ArrayBuffer>[]): string;
|
|
22
37
|
//#endregion
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2";
|
|
1
2
|
//#region src/domain/pinned-address.ts
|
|
2
3
|
const PIN_FRAGMENT = "#sha256=";
|
|
3
4
|
const HASH_SEPARATOR = ",";
|
|
4
5
|
const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
6
|
+
const HEX_RADIX = 16;
|
|
5
7
|
function hexToBytes(hex) {
|
|
6
8
|
const bytes = new Uint8Array(hex.length / 2);
|
|
7
9
|
for (let index = 0; index < bytes.length; index++) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
@@ -33,5 +35,49 @@ function parsePinnedAddress(address) {
|
|
|
33
35
|
sha256: hashes.map(hexToBytes)
|
|
34
36
|
};
|
|
35
37
|
}
|
|
38
|
+
/** The bytes in a SHA-256 hash. */
|
|
39
|
+
const SHA256_BYTES = 32;
|
|
40
|
+
/** The most hashes a node may announce at once. A browser is asked to accept any of them, and a longer list is a node misbehaving rather than one advertising a schedule. */
|
|
41
|
+
const MAX_ANNOUNCED_HASHES = 8;
|
|
42
|
+
/**
|
|
43
|
+
* The message a node sends a client, on a stream it opens to that client, to say which certificates it serves now and will serve next: a CBOR map with one key, `sha256`, holding an array of 32-byte hashes in the order they take over. It is authentic because it arrives on the session the client pinned, so no signature is needed.
|
|
44
|
+
*/
|
|
45
|
+
function encodeCertificateHashes(sha256Hex) {
|
|
46
|
+
return encode({ sha256: sha256Hex.map(hexToBytes) }, cdeEncodeOptions);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reads a message made by `encodeCertificateHashes`.
|
|
50
|
+
* @throws Error when it is not a map with a `sha256` array of one to MAX_ANNOUNCED_HASHES byte strings of 32 bytes each.
|
|
51
|
+
*/
|
|
52
|
+
function decodeCertificateHashes(bytes) {
|
|
53
|
+
const decoded = decode(bytes, cdeDecodeOptions);
|
|
54
|
+
if (typeof decoded !== "object" || decoded === null || !("sha256" in decoded) || !Array.isArray(decoded.sha256)) throw new Error("expected a map with a sha256 array");
|
|
55
|
+
const announced = decoded.sha256;
|
|
56
|
+
if (announced.length === 0 || announced.length > 8) throw new Error(`expected 1 to ${String(8)} hashes, got ${String(announced.length)}`);
|
|
57
|
+
const hashes = [];
|
|
58
|
+
for (const hash of announced) {
|
|
59
|
+
if (!(hash instanceof Uint8Array) || hash.length !== SHA256_BYTES) throw new Error(`each hash must be ${String(SHA256_BYTES)} bytes`);
|
|
60
|
+
hashes.push(new Uint8Array(hash));
|
|
61
|
+
}
|
|
62
|
+
return hashes;
|
|
63
|
+
}
|
|
64
|
+
/** `hashes` followed by those of `more` that are not already in it, so a client can pin what it was given and what it has learned since. */
|
|
65
|
+
function mergeHashes(hashes, more) {
|
|
66
|
+
const known = new Set(hashes.map(bytesToHex));
|
|
67
|
+
const merged = [...hashes];
|
|
68
|
+
for (const hash of more) if (!known.has(bytesToHex(hash))) {
|
|
69
|
+
known.add(bytesToHex(hash));
|
|
70
|
+
merged.push(hash);
|
|
71
|
+
}
|
|
72
|
+
return merged;
|
|
73
|
+
}
|
|
74
|
+
function bytesToHex(bytes) {
|
|
75
|
+
return Array.from(bytes, (byte) => byte.toString(HEX_RADIX).padStart(2, "0")).join("");
|
|
76
|
+
}
|
|
77
|
+
/** `address` with its pinned hashes replaced by `sha256`. */
|
|
78
|
+
function withPinnedHashes(address, sha256) {
|
|
79
|
+
const { url } = parsePinnedAddress(address);
|
|
80
|
+
return formatPinnedAddress(new URL(url).host, sha256.map(bytesToHex));
|
|
81
|
+
}
|
|
36
82
|
//#endregion
|
|
37
|
-
export { formatPinnedAddress, isPinnedAddress, parsePinnedAddress };
|
|
83
|
+
export { MAX_ANNOUNCED_HASHES, decodeCertificateHashes, encodeCertificateHashes, formatPinnedAddress, isPinnedAddress, mergeHashes, parsePinnedAddress, withPinnedHashes };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, N as Frame, et as PeerAdvert } from "../protocol-DCDae3z4.cjs";
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
2
|
+
import { t as Connection } from "../transport-DIXjoQKn.cjs";
|
|
3
3
|
import { AdvertExtensionPolicy } from "./advert-extension-policy.cjs";
|
|
4
4
|
import { Mailbox, MailboxRefusalReason } from "./hub-mailbox.cjs";
|
|
5
5
|
import { PeerAdvertVerifier } from "./peer-advert.cjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, N as Frame, et as PeerAdvert } from "../protocol-DCDae3z4.mjs";
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
2
|
+
import { t as Connection } from "../transport-B0GywEzG.mjs";
|
|
3
3
|
import { AdvertExtensionPolicy } from "./advert-extension-policy.mjs";
|
|
4
4
|
import { Mailbox, MailboxRefusalReason } from "./hub-mailbox.mjs";
|
|
5
5
|
import { PeerAdvertVerifier } from "./peer-advert.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { N as Frame } from "../protocol-DCDae3z4.cjs";
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
2
|
+
import { t as Connection } from "../transport-DIXjoQKn.cjs";
|
|
3
3
|
//#region src/domain/relay-use-gate.d.ts
|
|
4
4
|
export interface RelayUseAuthorizationTracker {
|
|
5
5
|
/** True once authorize() has been called for this exact connection and revoke() has not been called since. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { N as Frame } from "../protocol-DCDae3z4.mjs";
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
2
|
+
import { t as Connection } from "../transport-B0GywEzG.mjs";
|
|
3
3
|
//#region src/domain/relay-use-gate.d.ts
|
|
4
4
|
export interface RelayUseAuthorizationTracker {
|
|
5
5
|
/** True once authorize() has been called for this exact connection and revoke() has not been called since. */
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as Listener, r as Transport, t as Connection } from "../transport-
|
|
1
|
+
import { n as Listener, r as Transport, t as Connection } from "../transport-DIXjoQKn.cjs";
|
|
2
2
|
export { Connection, Listener, Transport };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as Listener, r as Transport, t as Connection } from "../transport-
|
|
1
|
+
import { n as Listener, r as Transport, t as Connection } from "../transport-B0GywEzG.mjs";
|
|
2
2
|
export { Connection, Listener, Transport };
|
|
@@ -10,6 +10,8 @@ interface Connection {
|
|
|
10
10
|
close: () => Promise<void>;
|
|
11
11
|
/** The peer's device-id, cryptographically authenticated by the adapter itself (never a value read off the wire) -- absent when the adapter has nothing to authenticate against (createTcpTransport) or the peer presented no credential to check (an accepting createTlsTransport connection whose peer sent no certificate). A caller that later receives an application-level claim of who the peer is (e.g. an `introduce`-style message) MUST treat a mismatch against this field as a spoofing attempt and refuse the claim, exactly the same "reject and destroy" obligation self-certifying claims elsewhere in this spec already carry. */
|
|
12
12
|
readonly peerDeviceId?: DeviceId;
|
|
13
|
+
/** The address to dial to reach this same peer again, when the adapter has learned a better one than the address this connection was dialled with: a node that rotates the certificate a browser pins tells each connection the certificates it will serve next, so a dial made after the address the user was given has aged still succeeds. Absent when the adapter learned nothing, in which case the dialled address stands. */
|
|
14
|
+
readonly redialAddress?: () => string;
|
|
13
15
|
/** Lets the event loop exit while this connection is still open and functioning -- a caller that wants I/O to keep working without keeping the process alive (matching Node's own `net.Socket.unref()`) calls this once. Adapters with no underlying handle to unref (an in-memory test double, a browser WebSocket) simply omit it. */
|
|
14
16
|
unref?: () => void;
|
|
15
17
|
}
|
|
@@ -10,6 +10,8 @@ interface Connection {
|
|
|
10
10
|
close: () => Promise<void>;
|
|
11
11
|
/** The peer's device-id, cryptographically authenticated by the adapter itself (never a value read off the wire) -- absent when the adapter has nothing to authenticate against (createTcpTransport) or the peer presented no credential to check (an accepting createTlsTransport connection whose peer sent no certificate). A caller that later receives an application-level claim of who the peer is (e.g. an `introduce`-style message) MUST treat a mismatch against this field as a spoofing attempt and refuse the claim, exactly the same "reject and destroy" obligation self-certifying claims elsewhere in this spec already carry. */
|
|
12
12
|
readonly peerDeviceId?: DeviceId;
|
|
13
|
+
/** The address to dial to reach this same peer again, when the adapter has learned a better one than the address this connection was dialled with: a node that rotates the certificate a browser pins tells each connection the certificates it will serve next, so a dial made after the address the user was given has aged still succeeds. Absent when the adapter learned nothing, in which case the dialled address stands. */
|
|
14
|
+
readonly redialAddress?: () => string;
|
|
13
15
|
/** Lets the event loop exit while this connection is still open and functioning -- a caller that wants I/O to keep working without keeping the process alive (matching Node's own `net.Socket.unref()`) calls this once. Adapters with no underlying handle to unref (an in-memory test double, a browser WebSocket) simply omit it. */
|
|
14
16
|
unref?: () => void;
|
|
15
17
|
}
|