wire-mesh-core 1.58.7 → 2.0.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/README.md +1 -0
- package/dist/adapters/frame-codec.d.cts +1 -1
- package/dist/adapters/frame-codec.d.mts +1 -1
- package/dist/adapters/node-identity.d.cts +2 -2
- package/dist/adapters/node-identity.d.mts +2 -2
- package/dist/adapters/tcp-transport.d.cts +1 -1
- package/dist/adapters/tcp-transport.d.mts +1 -1
- package/dist/adapters/threshold-identity.d.cts +2 -2
- package/dist/adapters/threshold-identity.d.mts +2 -2
- package/dist/adapters/threshold-wasm.d.cts +1 -1
- package/dist/adapters/threshold-wasm.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 +2 -2
- package/dist/domain/bulk.d.mts +2 -2
- package/dist/domain/capability-grant.d.cts +3 -3
- package/dist/domain/capability-grant.d.mts +3 -3
- package/dist/domain/capability-request.d.cts +2 -2
- package/dist/domain/capability-request.d.mts +2 -2
- package/dist/domain/data-sync.d.cts +2 -2
- package/dist/domain/data-sync.d.mts +2 -2
- package/dist/domain/device-id.d.cts +1 -1
- package/dist/domain/device-id.d.mts +1 -1
- package/dist/domain/direct-manage-request.d.cts +2 -2
- package/dist/domain/direct-manage-request.d.mts +2 -2
- package/dist/domain/gossip-expansion.d.cts +1 -1
- package/dist/domain/gossip-expansion.d.mts +1 -1
- package/dist/domain/gossip-extensions.cjs +1 -1
- package/dist/domain/gossip-extensions.mjs +1 -1
- package/dist/domain/grant-candidates.d.cts +1 -1
- package/dist/domain/grant-candidates.d.mts +1 -1
- package/dist/domain/handshake.d.cts +1 -1
- package/dist/domain/handshake.d.mts +1 -1
- package/dist/domain/mesh-session.cjs +13 -10
- package/dist/domain/mesh-session.d.cts +3 -3
- package/dist/domain/mesh-session.d.mts +3 -3
- package/dist/domain/mesh-session.mjs +13 -10
- package/dist/domain/notice-board.d.cts +3 -3
- package/dist/domain/notice-board.d.mts +3 -3
- package/dist/domain/path-trace.d.cts +1 -1
- package/dist/domain/path-trace.d.mts +1 -1
- package/dist/domain/peer-advert.cjs +61 -0
- package/dist/domain/peer-advert.d.cts +45 -0
- package/dist/domain/peer-advert.d.mts +45 -0
- package/dist/domain/peer-advert.mjs +55 -0
- package/dist/domain/relay-hub.cjs +93 -5
- package/dist/domain/relay-hub.d.cts +32 -3
- package/dist/domain/relay-hub.d.mts +32 -3
- package/dist/domain/relay-hub.mjs +93 -5
- package/dist/domain/relay-use-gate.d.cts +2 -2
- package/dist/domain/relay-use-gate.d.mts +2 -2
- package/dist/domain/revocation-view.d.cts +2 -2
- package/dist/domain/revocation-view.d.mts +2 -2
- package/dist/domain/room-rekey.d.cts +3 -3
- package/dist/domain/room-rekey.d.mts +3 -3
- package/dist/domain/room-token-verification.d.cts +1 -1
- package/dist/domain/room-token-verification.d.mts +1 -1
- package/dist/domain/room.d.cts +4 -4
- package/dist/domain/room.d.mts +4 -4
- package/dist/domain/shard-manifest.d.cts +1 -1
- package/dist/domain/shard-manifest.d.mts +1 -1
- package/dist/domain/tokens.d.cts +1 -1
- package/dist/domain/tokens.d.mts +1 -1
- package/dist/domain/topology-snapshot.d.cts +1 -1
- package/dist/domain/topology-snapshot.d.mts +1 -1
- package/dist/domain/topology.cjs +1 -1
- package/dist/domain/topology.d.cts +1 -1
- package/dist/domain/topology.d.mts +1 -1
- package/dist/domain/topology.mjs +1 -1
- package/dist/domain/webrtc-signaling.d.cts +2 -2
- package/dist/domain/webrtc-signaling.d.mts +2 -2
- package/dist/generated/protocol.cjs +3 -1
- package/dist/generated/protocol.d.cts +1 -1
- package/dist/generated/protocol.d.mts +1 -1
- package/dist/generated/protocol.mjs +3 -1
- package/dist/{gossip-extensions-Cdy5mQhY.mjs → gossip-extensions-BNiI44sU.mjs} +5 -2
- package/dist/{gossip-extensions-Dr_T7hk_.cjs → gossip-extensions-DRVTHKnR.cjs} +5 -2
- package/dist/{identity-BRlM-Krs.d.cts → identity-DImeXZcq.d.cts} +1 -1
- package/dist/{identity-DiuayfEd.d.mts → identity-Dc4xmwp3.d.mts} +1 -1
- package/dist/ports/identity.d.cts +1 -1
- package/dist/ports/identity.d.mts +1 -1
- package/dist/ports/transport.d.cts +1 -1
- package/dist/ports/transport.d.mts +1 -1
- package/dist/{protocol-BnbpCk0Z.d.cts → protocol-Bi2fzBCc.d.cts} +20 -0
- package/dist/{protocol-BnbpCk0Z.d.mts → protocol-Bi2fzBCc.d.mts} +20 -0
- package/dist/{room-token-verification-NjaqYY4k.d.mts → room-token-verification-CDXkm_Tg.d.cts} +2 -2
- package/dist/{room-token-verification-D9ogJTO3.d.cts → room-token-verification-CnHNaA8J.d.mts} +2 -2
- package/dist/{tokens-DtjdYS_a.d.mts → tokens-0r9ipK1r.d.mts} +2 -2
- package/dist/{tokens-BL2BzOmN.d.cts → tokens-BDVxEvl2.d.cts} +2 -2
- package/dist/{transport-BH8kGWXt.d.mts → transport-Cq_odHy3.d.cts} +1 -1
- package/dist/{transport-Ca_ZyyZe.d.cts → transport-GcJ4LYVE.d.mts} +1 -1
- package/package.json +5 -1
- package/wasm-dist/wire_mesh_threshold_wasm_bg.wasm +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { B as IdentityKey, D as DeviceId, et as PeerAdvert } from "../protocol-Bi2fzBCc.cjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-DImeXZcq.cjs";
|
|
3
|
+
//#region src/domain/peer-advert.d.ts
|
|
4
|
+
/** peer-advert's own signature field, the one entry the signature does not cover, named once here so the signing input and the reserved-extension-key guard cannot drift apart. */
|
|
5
|
+
export declare const PEER_ADVERT_SIGNATURE_KEY = "signature";
|
|
6
|
+
/** peer-advert's own self-certifying key field. Covered by the signature like every other entry, so an advert cannot be re-keyed after signing. */
|
|
7
|
+
export declare const PEER_ADVERT_IDENTITY_KEY_KEY = "identity-key";
|
|
8
|
+
/**
|
|
9
|
+
* The domain-separation prefix every peer-advert signature is computed over, per spec/transport.cddl: the ASCII string "wire-mesh/peer-advert/v1" followed by one zero byte.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately not tokens.cddl's COSE Sig_structure, which every other signature in this codebase reuses via sig1ToBeSigned. An advert's extension tail is an open `* tstr => any` map, so an attacker can craft an advert whose canonical encoding also satisfies token-claims; a Sig_structure over that encoding would then be replayable as a capability token's own signed payload. This prefix makes the two byte strings disjoint by construction, since a Sig_structure always begins with the CBOR head of a 4-element array (0x84) rather than the ASCII "w" this one starts with.
|
|
12
|
+
*/
|
|
13
|
+
export declare const PEER_ADVERT_SIGNING_CONTEXT: Uint8Array<ArrayBuffer>;
|
|
14
|
+
/**
|
|
15
|
+
* Everything a peer-advert's signature covers: the whole advert apart from `signature` itself. Spelled out as its own interface rather than derived with `Omit<PeerAdvert, "signature">`, which collapses to the bare index signature because peer-advert's open extension tail makes `keyof PeerAdvert` a plain `string`.
|
|
16
|
+
*
|
|
17
|
+
* A full PeerAdvert satisfies this structurally, which is what lets peerAdvertSigningInput take either an advert being signed or one being verified without a second overload.
|
|
18
|
+
*/
|
|
19
|
+
export interface UnsignedPeerAdvert {
|
|
20
|
+
readonly device: DeviceId;
|
|
21
|
+
readonly addresses: string[];
|
|
22
|
+
readonly "snapshot-seconds": number;
|
|
23
|
+
readonly "identity-key": IdentityKey;
|
|
24
|
+
/** peer-advert's own open extension tail (spec/CONVENTIONS.md's gossip-extension-namespacing convention). Every key here is signed over too, so nothing in the tail can be added, altered, or dropped without invalidating the advert. */
|
|
25
|
+
readonly [key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
/** The crypto primitives verification needs, and nothing else: an advert is self-certifying, so a verifier never consults its own device-id or private key to check one. Narrowed from IdentityPort so a relay hub, which has no reason to hold a signing identity, can be given a verifier without one. */
|
|
28
|
+
export type PeerAdvertVerifier = Pick<IdentityPort, "verify" | "deriveDeviceId">;
|
|
29
|
+
/** The signing half's own dependency: a node only ever signs adverts naming itself, which is exactly why a gateway forwards its local peers' adverts verbatim instead of re-signing them. */
|
|
30
|
+
export type PeerAdvertSigner = Pick<IdentityPort, "sign">;
|
|
31
|
+
/**
|
|
32
|
+
* The exact bytes a peer-advert's signature is computed over: PEER_ADVERT_SIGNING_CONTEXT followed by the canonical CDE encoding of the advert with its `signature` entry removed and nothing else changed.
|
|
33
|
+
*
|
|
34
|
+
* Takes the advert rather than a pre-stripped copy so a verifier derives the input from precisely the bytes it received, leaving no room for the signed and checked views of an advert to disagree.
|
|
35
|
+
*/
|
|
36
|
+
export declare function peerAdvertSigningInput(advert: Readonly<UnsignedPeerAdvert>): Uint8Array<ArrayBuffer>;
|
|
37
|
+
/** Signs an advert's content into the complete peer-advert that goes on the wire. The caller supplies `device` and `identity-key` itself rather than having them read off the signer, since the signer port carries only `sign`: a node building its own self-advert already holds both, and nothing else may legitimately sign an advert at all. */
|
|
38
|
+
export declare function signPeerAdvert(identity: Readonly<PeerAdvertSigner>, advert: Readonly<UnsignedPeerAdvert>): Promise<PeerAdvert>;
|
|
39
|
+
/**
|
|
40
|
+
* Both validity checks spec/transport.cddl states for a gossiped advert, in the order a verifier must apply them: the embedded identity-key self-certifies (sha256 of its public-key equals the advert's own `device`), and the signature verifies under that same key over peerAdvertSigningInput's bytes.
|
|
41
|
+
*
|
|
42
|
+
* Returns a verdict for every input, including a hostile one, and never throws. An advert's `alg` and `public-key` are attacker-chosen (identity-key constrains them to an int and a bstr, nothing more), so importing the key can legitimately fail for an algorithm no verifier implements or for key bytes of the wrong length. An unverifiable advert is a refused advert, exactly as if its signature had simply been wrong, which is why the import failure is turned into `false` here rather than propagated to a caller iterating a frame's other adverts.
|
|
43
|
+
*/
|
|
44
|
+
export declare function verifyPeerAdvert(identity: Readonly<PeerAdvertVerifier>, advert: Readonly<PeerAdvert>): Promise<boolean>;
|
|
45
|
+
//#endregion
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { B as IdentityKey, D as DeviceId, et as PeerAdvert } from "../protocol-Bi2fzBCc.mjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-Dc4xmwp3.mjs";
|
|
3
|
+
//#region src/domain/peer-advert.d.ts
|
|
4
|
+
/** peer-advert's own signature field, the one entry the signature does not cover, named once here so the signing input and the reserved-extension-key guard cannot drift apart. */
|
|
5
|
+
export declare const PEER_ADVERT_SIGNATURE_KEY = "signature";
|
|
6
|
+
/** peer-advert's own self-certifying key field. Covered by the signature like every other entry, so an advert cannot be re-keyed after signing. */
|
|
7
|
+
export declare const PEER_ADVERT_IDENTITY_KEY_KEY = "identity-key";
|
|
8
|
+
/**
|
|
9
|
+
* The domain-separation prefix every peer-advert signature is computed over, per spec/transport.cddl: the ASCII string "wire-mesh/peer-advert/v1" followed by one zero byte.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately not tokens.cddl's COSE Sig_structure, which every other signature in this codebase reuses via sig1ToBeSigned. An advert's extension tail is an open `* tstr => any` map, so an attacker can craft an advert whose canonical encoding also satisfies token-claims; a Sig_structure over that encoding would then be replayable as a capability token's own signed payload. This prefix makes the two byte strings disjoint by construction, since a Sig_structure always begins with the CBOR head of a 4-element array (0x84) rather than the ASCII "w" this one starts with.
|
|
12
|
+
*/
|
|
13
|
+
export declare const PEER_ADVERT_SIGNING_CONTEXT: Uint8Array<ArrayBuffer>;
|
|
14
|
+
/**
|
|
15
|
+
* Everything a peer-advert's signature covers: the whole advert apart from `signature` itself. Spelled out as its own interface rather than derived with `Omit<PeerAdvert, "signature">`, which collapses to the bare index signature because peer-advert's open extension tail makes `keyof PeerAdvert` a plain `string`.
|
|
16
|
+
*
|
|
17
|
+
* A full PeerAdvert satisfies this structurally, which is what lets peerAdvertSigningInput take either an advert being signed or one being verified without a second overload.
|
|
18
|
+
*/
|
|
19
|
+
export interface UnsignedPeerAdvert {
|
|
20
|
+
readonly device: DeviceId;
|
|
21
|
+
readonly addresses: string[];
|
|
22
|
+
readonly "snapshot-seconds": number;
|
|
23
|
+
readonly "identity-key": IdentityKey;
|
|
24
|
+
/** peer-advert's own open extension tail (spec/CONVENTIONS.md's gossip-extension-namespacing convention). Every key here is signed over too, so nothing in the tail can be added, altered, or dropped without invalidating the advert. */
|
|
25
|
+
readonly [key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
/** The crypto primitives verification needs, and nothing else: an advert is self-certifying, so a verifier never consults its own device-id or private key to check one. Narrowed from IdentityPort so a relay hub, which has no reason to hold a signing identity, can be given a verifier without one. */
|
|
28
|
+
export type PeerAdvertVerifier = Pick<IdentityPort, "verify" | "deriveDeviceId">;
|
|
29
|
+
/** The signing half's own dependency: a node only ever signs adverts naming itself, which is exactly why a gateway forwards its local peers' adverts verbatim instead of re-signing them. */
|
|
30
|
+
export type PeerAdvertSigner = Pick<IdentityPort, "sign">;
|
|
31
|
+
/**
|
|
32
|
+
* The exact bytes a peer-advert's signature is computed over: PEER_ADVERT_SIGNING_CONTEXT followed by the canonical CDE encoding of the advert with its `signature` entry removed and nothing else changed.
|
|
33
|
+
*
|
|
34
|
+
* Takes the advert rather than a pre-stripped copy so a verifier derives the input from precisely the bytes it received, leaving no room for the signed and checked views of an advert to disagree.
|
|
35
|
+
*/
|
|
36
|
+
export declare function peerAdvertSigningInput(advert: Readonly<UnsignedPeerAdvert>): Uint8Array<ArrayBuffer>;
|
|
37
|
+
/** Signs an advert's content into the complete peer-advert that goes on the wire. The caller supplies `device` and `identity-key` itself rather than having them read off the signer, since the signer port carries only `sign`: a node building its own self-advert already holds both, and nothing else may legitimately sign an advert at all. */
|
|
38
|
+
export declare function signPeerAdvert(identity: Readonly<PeerAdvertSigner>, advert: Readonly<UnsignedPeerAdvert>): Promise<PeerAdvert>;
|
|
39
|
+
/**
|
|
40
|
+
* Both validity checks spec/transport.cddl states for a gossiped advert, in the order a verifier must apply them: the embedded identity-key self-certifies (sha256 of its public-key equals the advert's own `device`), and the signature verifies under that same key over peerAdvertSigningInput's bytes.
|
|
41
|
+
*
|
|
42
|
+
* Returns a verdict for every input, including a hostile one, and never throws. An advert's `alg` and `public-key` are attacker-chosen (identity-key constrains them to an int and a bstr, nothing more), so importing the key can legitimately fail for an algorithm no verifier implements or for key bytes of the wrong length. An unverifiable advert is a refused advert, exactly as if its signature had simply been wrong, which is why the import failure is turned into `false` here rather than propagated to a caller iterating a frame's other adverts.
|
|
43
|
+
*/
|
|
44
|
+
export declare function verifyPeerAdvert(identity: Readonly<PeerAdvertVerifier>, advert: Readonly<PeerAdvert>): Promise<boolean>;
|
|
45
|
+
//#endregion
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { t as bytesEqual } from "../token-scope-Z4bmci4M.mjs";
|
|
2
|
+
import { cdeEncodeOptions, encode } from "cbor2";
|
|
3
|
+
//#region src/domain/peer-advert.ts
|
|
4
|
+
/** peer-advert's own signature field, the one entry the signature does not cover, named once here so the signing input and the reserved-extension-key guard cannot drift apart. */
|
|
5
|
+
const PEER_ADVERT_SIGNATURE_KEY = "signature";
|
|
6
|
+
/** peer-advert's own self-certifying key field. Covered by the signature like every other entry, so an advert cannot be re-keyed after signing. */
|
|
7
|
+
const PEER_ADVERT_IDENTITY_KEY_KEY = "identity-key";
|
|
8
|
+
/**
|
|
9
|
+
* The domain-separation prefix every peer-advert signature is computed over, per spec/transport.cddl: the ASCII string "wire-mesh/peer-advert/v1" followed by one zero byte.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately not tokens.cddl's COSE Sig_structure, which every other signature in this codebase reuses via sig1ToBeSigned. An advert's extension tail is an open `* tstr => any` map, so an attacker can craft an advert whose canonical encoding also satisfies token-claims; a Sig_structure over that encoding would then be replayable as a capability token's own signed payload. This prefix makes the two byte strings disjoint by construction, since a Sig_structure always begins with the CBOR head of a 4-element array (0x84) rather than the ASCII "w" this one starts with.
|
|
12
|
+
*/
|
|
13
|
+
const PEER_ADVERT_SIGNING_CONTEXT = new TextEncoder().encode("wire-mesh/peer-advert/v1\0");
|
|
14
|
+
/**
|
|
15
|
+
* The exact bytes a peer-advert's signature is computed over: PEER_ADVERT_SIGNING_CONTEXT followed by the canonical CDE encoding of the advert with its `signature` entry removed and nothing else changed.
|
|
16
|
+
*
|
|
17
|
+
* Takes the advert rather than a pre-stripped copy so a verifier derives the input from precisely the bytes it received, leaving no room for the signed and checked views of an advert to disagree.
|
|
18
|
+
*/
|
|
19
|
+
function peerAdvertSigningInput(advert) {
|
|
20
|
+
const content = {};
|
|
21
|
+
for (const [key, value] of Object.entries(advert)) {
|
|
22
|
+
if (key === "signature") continue;
|
|
23
|
+
content[key] = value;
|
|
24
|
+
}
|
|
25
|
+
const encoded = encode(content, cdeEncodeOptions);
|
|
26
|
+
const input = new Uint8Array(PEER_ADVERT_SIGNING_CONTEXT.length + encoded.length);
|
|
27
|
+
input.set(PEER_ADVERT_SIGNING_CONTEXT, 0);
|
|
28
|
+
input.set(encoded, PEER_ADVERT_SIGNING_CONTEXT.length);
|
|
29
|
+
return input;
|
|
30
|
+
}
|
|
31
|
+
/** Signs an advert's content into the complete peer-advert that goes on the wire. The caller supplies `device` and `identity-key` itself rather than having them read off the signer, since the signer port carries only `sign`: a node building its own self-advert already holds both, and nothing else may legitimately sign an advert at all. */
|
|
32
|
+
async function signPeerAdvert(identity, advert) {
|
|
33
|
+
const signature = await identity.sign(peerAdvertSigningInput(advert));
|
|
34
|
+
return {
|
|
35
|
+
...advert,
|
|
36
|
+
[PEER_ADVERT_SIGNATURE_KEY]: signature
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Both validity checks spec/transport.cddl states for a gossiped advert, in the order a verifier must apply them: the embedded identity-key self-certifies (sha256 of its public-key equals the advert's own `device`), and the signature verifies under that same key over peerAdvertSigningInput's bytes.
|
|
41
|
+
*
|
|
42
|
+
* Returns a verdict for every input, including a hostile one, and never throws. An advert's `alg` and `public-key` are attacker-chosen (identity-key constrains them to an int and a bstr, nothing more), so importing the key can legitimately fail for an algorithm no verifier implements or for key bytes of the wrong length. An unverifiable advert is a refused advert, exactly as if its signature had simply been wrong, which is why the import failure is turned into `false` here rather than propagated to a caller iterating a frame's other adverts.
|
|
43
|
+
*/
|
|
44
|
+
async function verifyPeerAdvert(identity, advert) {
|
|
45
|
+
const key = advert[PEER_ADVERT_IDENTITY_KEY_KEY];
|
|
46
|
+
try {
|
|
47
|
+
const derived = await identity.deriveDeviceId(key["public-key"]);
|
|
48
|
+
if (!bytesEqual(derived, advert.device)) return false;
|
|
49
|
+
return await identity.verify(key, peerAdvertSigningInput(advert), advert[PEER_ADVERT_SIGNATURE_KEY]);
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { PEER_ADVERT_IDENTITY_KEY_KEY, PEER_ADVERT_SIGNATURE_KEY, PEER_ADVERT_SIGNING_CONTEXT, peerAdvertSigningInput, signPeerAdvert, verifyPeerAdvert };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_domain_peer_advert = require("./peer-advert.cjs");
|
|
2
3
|
//#region src/domain/relay-hub.ts
|
|
3
4
|
const HEX_RADIX = 16;
|
|
4
5
|
const HEX_DIGITS_PER_BYTE = 2;
|
|
@@ -7,12 +8,31 @@ function deviceKey(device) {
|
|
|
7
8
|
for (const byte of device) key += byte.toString(HEX_RADIX).padStart(HEX_DIGITS_PER_BYTE, "0");
|
|
8
9
|
return key;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
/** The exact inverse of deviceKey, so a pairing map's own hex keys can be exported as the device-ids they were built from without holding a second copy of the bytes on the forwarding path. */
|
|
12
|
+
function deviceFromKey(key) {
|
|
13
|
+
const device = new Uint8Array(key.length / HEX_DIGITS_PER_BYTE);
|
|
14
|
+
for (let i = 0; i < device.length; i++) device[i] = Number.parseInt(key.slice(i * HEX_DIGITS_PER_BYTE, (i + 1) * HEX_DIGITS_PER_BYTE), HEX_RADIX);
|
|
15
|
+
return device;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether an already-verified advert may take over the registration a device currently holds (wire-mesh#225).
|
|
19
|
+
*
|
|
20
|
+
* Strictly newer when it arrives on a different connection from the one holding the registration, so replaying a victim's genuine advert, which carries the victim's own `snapshot-seconds` unchanged since altering it would break the signature, can never steal its route. Newer-or-equal from the connection that already holds it, so an unchanged heartbeat from the rightful owner still refreshes the addresses and extensions the hub replays in catch-up frames.
|
|
21
|
+
*/
|
|
22
|
+
function supersedes(registered, advert, connection) {
|
|
23
|
+
const incoming = advert["snapshot-seconds"];
|
|
24
|
+
const held = registered.advert["snapshot-seconds"];
|
|
25
|
+
return registered.connection === connection ? incoming >= held : incoming > held;
|
|
26
|
+
}
|
|
27
|
+
function createRelayHub(options) {
|
|
11
28
|
const connections = /* @__PURE__ */ new Set();
|
|
12
29
|
const devices = /* @__PURE__ */ new Map();
|
|
13
30
|
const connectionDevice = /* @__PURE__ */ new Map();
|
|
14
31
|
const pairings = /* @__PURE__ */ new Map();
|
|
15
32
|
const mostRecentPairing = /* @__PURE__ */ new Map();
|
|
33
|
+
function stateChanged(connection) {
|
|
34
|
+
options.onConnectionStateChanged?.(connection);
|
|
35
|
+
}
|
|
16
36
|
function pairingsOf(connection) {
|
|
17
37
|
const existing = pairings.get(connection);
|
|
18
38
|
if (existing) return existing;
|
|
@@ -38,24 +58,88 @@ function createRelayHub() {
|
|
|
38
58
|
const peerOwn = pairings.get(peer);
|
|
39
59
|
if (peerOwn && ownDevice !== void 0) peerOwn.delete(deviceKey(ownDevice));
|
|
40
60
|
if (mostRecentPairing.get(peer) === connection) mostRecentPairing.delete(peer);
|
|
61
|
+
stateChanged(peer);
|
|
41
62
|
}
|
|
42
63
|
pairings.delete(connection);
|
|
43
64
|
}
|
|
44
65
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
66
|
+
function exportConnection(connection) {
|
|
67
|
+
const device = connectionDevice.get(connection);
|
|
68
|
+
if (device === void 0) return;
|
|
69
|
+
const adverts = [];
|
|
70
|
+
for (const registration of devices.values()) if (registration.connection === connection) adverts.push(registration.advert);
|
|
71
|
+
const own = pairings.get(connection);
|
|
72
|
+
const pairedDevices = [...own?.keys() ?? []].map(deviceFromKey);
|
|
73
|
+
const recentPeer = mostRecentPairing.get(connection);
|
|
74
|
+
let mostRecentDevice;
|
|
75
|
+
if (own && recentPeer) {
|
|
76
|
+
for (const [key, peer] of own) if (peer === recentPeer) {
|
|
77
|
+
mostRecentDevice = deviceFromKey(key);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
device,
|
|
83
|
+
adverts,
|
|
84
|
+
pairedDevices,
|
|
85
|
+
...mostRecentDevice !== void 0 ? { mostRecentDevice } : {}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function restoreConnections(entries) {
|
|
89
|
+
const byDevice = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const { connection, state } of entries) {
|
|
91
|
+
connections.add(connection);
|
|
92
|
+
connectionDevice.set(connection, state.device);
|
|
93
|
+
byDevice.set(deviceKey(state.device), connection);
|
|
94
|
+
for (const advert of state.adverts) devices.set(deviceKey(advert.device), {
|
|
48
95
|
connection,
|
|
49
96
|
advert
|
|
50
97
|
});
|
|
98
|
+
}
|
|
99
|
+
for (const { connection, state } of entries) for (const paired of state.pairedDevices) {
|
|
100
|
+
const peer = byDevice.get(deviceKey(paired));
|
|
101
|
+
if (peer === void 0) continue;
|
|
102
|
+
pairingsOf(connection).set(deviceKey(paired), peer);
|
|
103
|
+
pairingsOf(peer).set(deviceKey(state.device), connection);
|
|
104
|
+
}
|
|
105
|
+
for (const { connection, state } of entries) {
|
|
106
|
+
const recent = state.mostRecentDevice;
|
|
107
|
+
if (recent === void 0) continue;
|
|
108
|
+
const peer = byDevice.get(deviceKey(recent));
|
|
109
|
+
if (peer !== void 0) mostRecentPairing.set(connection, peer);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function handleFrame(connection, frame) {
|
|
113
|
+
if (frame.type === "gossip") {
|
|
114
|
+
const supplanted = /* @__PURE__ */ new Set();
|
|
115
|
+
const accepted = [];
|
|
116
|
+
for (const advert of frame.peers) {
|
|
117
|
+
if (!await require_domain_peer_advert.verifyPeerAdvert(options.identity, advert)) continue;
|
|
118
|
+
const key = deviceKey(advert.device);
|
|
119
|
+
const previous = devices.get(key);
|
|
120
|
+
if (previous !== void 0 && !supersedes(previous, advert, connection)) continue;
|
|
121
|
+
if (previous !== void 0 && previous.connection !== connection) supplanted.add(previous.connection);
|
|
122
|
+
devices.set(key, {
|
|
123
|
+
connection,
|
|
124
|
+
advert
|
|
125
|
+
});
|
|
126
|
+
accepted.push(advert);
|
|
127
|
+
}
|
|
128
|
+
if (accepted.length === 0) return;
|
|
51
129
|
for (const registration of devices.values()) if (registration.connection === connection) {
|
|
52
130
|
connectionDevice.set(connection, registration.advert.device);
|
|
53
131
|
break;
|
|
54
132
|
}
|
|
133
|
+
stateChanged(connection);
|
|
134
|
+
for (const other of supplanted) stateChanged(other);
|
|
135
|
+
const forwarded = {
|
|
136
|
+
type: "gossip",
|
|
137
|
+
peers: accepted
|
|
138
|
+
};
|
|
55
139
|
for (const other of connections) {
|
|
56
140
|
if (other === connection) continue;
|
|
57
141
|
try {
|
|
58
|
-
await other.send(
|
|
142
|
+
await other.send(forwarded);
|
|
59
143
|
} catch {}
|
|
60
144
|
}
|
|
61
145
|
const catchUp = [];
|
|
@@ -74,6 +158,8 @@ function createRelayHub() {
|
|
|
74
158
|
const initiatorDevice = connectionDevice.get(connection);
|
|
75
159
|
if (initiatorDevice === void 0) return;
|
|
76
160
|
addPairing(connection, initiatorDevice, registration.connection, registration.advert.device);
|
|
161
|
+
stateChanged(connection);
|
|
162
|
+
stateChanged(registration.connection);
|
|
77
163
|
await registration.connection.send({
|
|
78
164
|
type: "relay-inbound",
|
|
79
165
|
"source-device": initiatorDevice
|
|
@@ -116,6 +202,8 @@ function createRelayHub() {
|
|
|
116
202
|
},
|
|
117
203
|
onFrame: handleFrame,
|
|
118
204
|
onDisconnect: forgetConnection,
|
|
205
|
+
exportConnection,
|
|
206
|
+
restoreConnections,
|
|
119
207
|
stop() {
|
|
120
208
|
connections.clear();
|
|
121
209
|
devices.clear();
|
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
import { N as Frame } from "../protocol-
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
1
|
+
import { D as DeviceId, N as Frame, et as PeerAdvert } from "../protocol-Bi2fzBCc.cjs";
|
|
2
|
+
import { t as Connection } from "../transport-Cq_odHy3.cjs";
|
|
3
|
+
import { PeerAdvertVerifier } from "./peer-advert.cjs";
|
|
3
4
|
//#region src/domain/relay-hub.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* One connection's relay state in serialisable form: everything a hub that never saw the connection register needs, alongside the still-open connection itself, to answer its next frame exactly as the hub that established the state would have. Only device-ids and adverts appear, never a Connection, so a host can persist this wherever it keeps per-connection state.
|
|
7
|
+
*/
|
|
8
|
+
export interface RelayConnectionState {
|
|
9
|
+
/** The device-id the hub attributes to the connection itself: what its relay-data frames are stamped with as `from-device`, and what a peer's own pairing map keys this connection by. */
|
|
10
|
+
readonly device: DeviceId;
|
|
11
|
+
/** Every peer-advert still registered against this connection in the hub's device directory, in registration order, so a restored hub can resolve a relay-connect naming one of them and can replay them in its gossip catch-up. Empty when a later connection has taken over every device-id this one advertised. */
|
|
12
|
+
readonly adverts: readonly PeerAdvert[];
|
|
13
|
+
/** The device-id of each peer this connection currently holds a relay pairing with. Order carries no meaning; the unaddressed-relay-data route is `mostRecentDevice` alone. */
|
|
14
|
+
readonly pairedDevices: readonly DeviceId[];
|
|
15
|
+
/** The peer a relay-data frame carrying no `to-device` routes to. Absent when the connection has no such fallback, either because it holds no pairings or because the peer that was most recent has since disconnected. */
|
|
16
|
+
readonly mostRecentDevice?: DeviceId;
|
|
17
|
+
}
|
|
18
|
+
/** One surviving connection paired with what a previous hub instance knew about it, as restoreConnections takes it. */
|
|
19
|
+
export interface RelayConnectionRestore {
|
|
20
|
+
readonly connection: Readonly<Connection>;
|
|
21
|
+
readonly state: RelayConnectionState;
|
|
22
|
+
}
|
|
23
|
+
export interface RelayHubOptions {
|
|
24
|
+
/** The crypto primitives this hub verifies every gossiped advert with (wire-mesh#225). Narrowed to verify/deriveDeviceId rather than a full IdentityPort because an advert is self-certifying: a hub checks one against the key the advert itself carries, and never needs a signing identity of its own to do it. */
|
|
25
|
+
readonly identity: Readonly<PeerAdvertVerifier>;
|
|
26
|
+
/** Called whenever the value exportConnection returns for a connection changes: it gossiped an advert, gained or lost a pairing, or had one of its adverts taken over by another connection. A host whose own instance can be torn down while its connections stay open (the Cloudflare hub's hibernating Durable Object) reads the new value back with exportConnection, persists it against the connection, and hands it to a later instance through restoreConnections. */
|
|
27
|
+
readonly onConnectionStateChanged?: (connection: Readonly<Connection>) => void;
|
|
28
|
+
}
|
|
4
29
|
export interface RelayHub {
|
|
5
30
|
/** Drives one accepted connection until it closes: registers gossip-advertised devices, re-broadcasts each gossip frame to every other connected client and replies to the gossiping connection with a catch-up frame of every other already-known device, answers relay-connect by pairing and notifying the target, forwards relay-data within established pairings, and replies to a bare ping with a bare pong (wire-mesh#181) so a client can time its own sender-to-hub leg independent of anything being relayed. Resolves when the connection's frame stream ends. Internally just registerConnection followed by a loop of onFrame calls and a final onDisconnect -- kept as its own method since a dedicated hub deployment (wire-mesh/cloudflare-hub) has nothing else driving the connection and wants the whole lifecycle in one call. */
|
|
6
31
|
handleConnection: (connection: Readonly<Connection>) => Promise<void>;
|
|
@@ -10,8 +35,12 @@ export interface RelayHub {
|
|
|
10
35
|
onFrame: (connection: Readonly<Connection>, frame: Frame) => Promise<void>;
|
|
11
36
|
/** Cleans up all registry and pairing state for one connection once its own frame stream has ended -- the onSessionEnd counterpart registerConnection/onFrame needs for the shared-consumption case, since this hub's registry is keyed by Connection and has no other way to learn a connection is gone. */
|
|
12
37
|
onDisconnect: (connection: Readonly<Connection>) => void;
|
|
38
|
+
/** The serialisable relay state of one registered connection, or undefined when it holds none worth keeping: a connection that has never gossiped an advert can neither be named by a relay-connect nor send a routable relay-data frame, so there is nothing for a later hub to restore. */
|
|
39
|
+
exportConnection: (connection: Readonly<Connection>) => RelayConnectionState | undefined;
|
|
40
|
+
/** Rebuilds device-registry and pairing state for connections that outlived the hub instance which established it, so a frame arriving on one of them routes as it did before rather than being dropped as unknown. Each entry's connection is registered as if registerConnection had been called for it. Pairings resolve within the batch: a paired device-id no entry claims belonged to a peer that disconnected while no instance was running, and its pairing is correctly left out. Meant for a freshly created hub, before it handles any frame. */
|
|
41
|
+
restoreConnections: (entries: readonly RelayConnectionRestore[]) => void;
|
|
13
42
|
/** Drops all registry and pairing state -- used by tests and by transport teardown. */
|
|
14
43
|
stop: () => void;
|
|
15
44
|
}
|
|
16
|
-
export declare function createRelayHub(): RelayHub;
|
|
45
|
+
export declare function createRelayHub(options: Readonly<RelayHubOptions>): RelayHub;
|
|
17
46
|
//#endregion
|
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
import { N as Frame } from "../protocol-
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
1
|
+
import { D as DeviceId, N as Frame, et as PeerAdvert } from "../protocol-Bi2fzBCc.mjs";
|
|
2
|
+
import { t as Connection } from "../transport-GcJ4LYVE.mjs";
|
|
3
|
+
import { PeerAdvertVerifier } from "./peer-advert.mjs";
|
|
3
4
|
//#region src/domain/relay-hub.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* One connection's relay state in serialisable form: everything a hub that never saw the connection register needs, alongside the still-open connection itself, to answer its next frame exactly as the hub that established the state would have. Only device-ids and adverts appear, never a Connection, so a host can persist this wherever it keeps per-connection state.
|
|
7
|
+
*/
|
|
8
|
+
export interface RelayConnectionState {
|
|
9
|
+
/** The device-id the hub attributes to the connection itself: what its relay-data frames are stamped with as `from-device`, and what a peer's own pairing map keys this connection by. */
|
|
10
|
+
readonly device: DeviceId;
|
|
11
|
+
/** Every peer-advert still registered against this connection in the hub's device directory, in registration order, so a restored hub can resolve a relay-connect naming one of them and can replay them in its gossip catch-up. Empty when a later connection has taken over every device-id this one advertised. */
|
|
12
|
+
readonly adverts: readonly PeerAdvert[];
|
|
13
|
+
/** The device-id of each peer this connection currently holds a relay pairing with. Order carries no meaning; the unaddressed-relay-data route is `mostRecentDevice` alone. */
|
|
14
|
+
readonly pairedDevices: readonly DeviceId[];
|
|
15
|
+
/** The peer a relay-data frame carrying no `to-device` routes to. Absent when the connection has no such fallback, either because it holds no pairings or because the peer that was most recent has since disconnected. */
|
|
16
|
+
readonly mostRecentDevice?: DeviceId;
|
|
17
|
+
}
|
|
18
|
+
/** One surviving connection paired with what a previous hub instance knew about it, as restoreConnections takes it. */
|
|
19
|
+
export interface RelayConnectionRestore {
|
|
20
|
+
readonly connection: Readonly<Connection>;
|
|
21
|
+
readonly state: RelayConnectionState;
|
|
22
|
+
}
|
|
23
|
+
export interface RelayHubOptions {
|
|
24
|
+
/** The crypto primitives this hub verifies every gossiped advert with (wire-mesh#225). Narrowed to verify/deriveDeviceId rather than a full IdentityPort because an advert is self-certifying: a hub checks one against the key the advert itself carries, and never needs a signing identity of its own to do it. */
|
|
25
|
+
readonly identity: Readonly<PeerAdvertVerifier>;
|
|
26
|
+
/** Called whenever the value exportConnection returns for a connection changes: it gossiped an advert, gained or lost a pairing, or had one of its adverts taken over by another connection. A host whose own instance can be torn down while its connections stay open (the Cloudflare hub's hibernating Durable Object) reads the new value back with exportConnection, persists it against the connection, and hands it to a later instance through restoreConnections. */
|
|
27
|
+
readonly onConnectionStateChanged?: (connection: Readonly<Connection>) => void;
|
|
28
|
+
}
|
|
4
29
|
export interface RelayHub {
|
|
5
30
|
/** Drives one accepted connection until it closes: registers gossip-advertised devices, re-broadcasts each gossip frame to every other connected client and replies to the gossiping connection with a catch-up frame of every other already-known device, answers relay-connect by pairing and notifying the target, forwards relay-data within established pairings, and replies to a bare ping with a bare pong (wire-mesh#181) so a client can time its own sender-to-hub leg independent of anything being relayed. Resolves when the connection's frame stream ends. Internally just registerConnection followed by a loop of onFrame calls and a final onDisconnect -- kept as its own method since a dedicated hub deployment (wire-mesh/cloudflare-hub) has nothing else driving the connection and wants the whole lifecycle in one call. */
|
|
6
31
|
handleConnection: (connection: Readonly<Connection>) => Promise<void>;
|
|
@@ -10,8 +35,12 @@ export interface RelayHub {
|
|
|
10
35
|
onFrame: (connection: Readonly<Connection>, frame: Frame) => Promise<void>;
|
|
11
36
|
/** Cleans up all registry and pairing state for one connection once its own frame stream has ended -- the onSessionEnd counterpart registerConnection/onFrame needs for the shared-consumption case, since this hub's registry is keyed by Connection and has no other way to learn a connection is gone. */
|
|
12
37
|
onDisconnect: (connection: Readonly<Connection>) => void;
|
|
38
|
+
/** The serialisable relay state of one registered connection, or undefined when it holds none worth keeping: a connection that has never gossiped an advert can neither be named by a relay-connect nor send a routable relay-data frame, so there is nothing for a later hub to restore. */
|
|
39
|
+
exportConnection: (connection: Readonly<Connection>) => RelayConnectionState | undefined;
|
|
40
|
+
/** Rebuilds device-registry and pairing state for connections that outlived the hub instance which established it, so a frame arriving on one of them routes as it did before rather than being dropped as unknown. Each entry's connection is registered as if registerConnection had been called for it. Pairings resolve within the batch: a paired device-id no entry claims belonged to a peer that disconnected while no instance was running, and its pairing is correctly left out. Meant for a freshly created hub, before it handles any frame. */
|
|
41
|
+
restoreConnections: (entries: readonly RelayConnectionRestore[]) => void;
|
|
13
42
|
/** Drops all registry and pairing state -- used by tests and by transport teardown. */
|
|
14
43
|
stop: () => void;
|
|
15
44
|
}
|
|
16
|
-
export declare function createRelayHub(): RelayHub;
|
|
45
|
+
export declare function createRelayHub(options: Readonly<RelayHubOptions>): RelayHub;
|
|
17
46
|
//#endregion
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { verifyPeerAdvert } from "./peer-advert.mjs";
|
|
1
2
|
//#region src/domain/relay-hub.ts
|
|
2
3
|
const HEX_RADIX = 16;
|
|
3
4
|
const HEX_DIGITS_PER_BYTE = 2;
|
|
@@ -6,12 +7,31 @@ function deviceKey(device) {
|
|
|
6
7
|
for (const byte of device) key += byte.toString(HEX_RADIX).padStart(HEX_DIGITS_PER_BYTE, "0");
|
|
7
8
|
return key;
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
+
/** The exact inverse of deviceKey, so a pairing map's own hex keys can be exported as the device-ids they were built from without holding a second copy of the bytes on the forwarding path. */
|
|
11
|
+
function deviceFromKey(key) {
|
|
12
|
+
const device = new Uint8Array(key.length / HEX_DIGITS_PER_BYTE);
|
|
13
|
+
for (let i = 0; i < device.length; i++) device[i] = Number.parseInt(key.slice(i * HEX_DIGITS_PER_BYTE, (i + 1) * HEX_DIGITS_PER_BYTE), HEX_RADIX);
|
|
14
|
+
return device;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether an already-verified advert may take over the registration a device currently holds (wire-mesh#225).
|
|
18
|
+
*
|
|
19
|
+
* Strictly newer when it arrives on a different connection from the one holding the registration, so replaying a victim's genuine advert, which carries the victim's own `snapshot-seconds` unchanged since altering it would break the signature, can never steal its route. Newer-or-equal from the connection that already holds it, so an unchanged heartbeat from the rightful owner still refreshes the addresses and extensions the hub replays in catch-up frames.
|
|
20
|
+
*/
|
|
21
|
+
function supersedes(registered, advert, connection) {
|
|
22
|
+
const incoming = advert["snapshot-seconds"];
|
|
23
|
+
const held = registered.advert["snapshot-seconds"];
|
|
24
|
+
return registered.connection === connection ? incoming >= held : incoming > held;
|
|
25
|
+
}
|
|
26
|
+
function createRelayHub(options) {
|
|
10
27
|
const connections = /* @__PURE__ */ new Set();
|
|
11
28
|
const devices = /* @__PURE__ */ new Map();
|
|
12
29
|
const connectionDevice = /* @__PURE__ */ new Map();
|
|
13
30
|
const pairings = /* @__PURE__ */ new Map();
|
|
14
31
|
const mostRecentPairing = /* @__PURE__ */ new Map();
|
|
32
|
+
function stateChanged(connection) {
|
|
33
|
+
options.onConnectionStateChanged?.(connection);
|
|
34
|
+
}
|
|
15
35
|
function pairingsOf(connection) {
|
|
16
36
|
const existing = pairings.get(connection);
|
|
17
37
|
if (existing) return existing;
|
|
@@ -37,24 +57,88 @@ function createRelayHub() {
|
|
|
37
57
|
const peerOwn = pairings.get(peer);
|
|
38
58
|
if (peerOwn && ownDevice !== void 0) peerOwn.delete(deviceKey(ownDevice));
|
|
39
59
|
if (mostRecentPairing.get(peer) === connection) mostRecentPairing.delete(peer);
|
|
60
|
+
stateChanged(peer);
|
|
40
61
|
}
|
|
41
62
|
pairings.delete(connection);
|
|
42
63
|
}
|
|
43
64
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
function exportConnection(connection) {
|
|
66
|
+
const device = connectionDevice.get(connection);
|
|
67
|
+
if (device === void 0) return;
|
|
68
|
+
const adverts = [];
|
|
69
|
+
for (const registration of devices.values()) if (registration.connection === connection) adverts.push(registration.advert);
|
|
70
|
+
const own = pairings.get(connection);
|
|
71
|
+
const pairedDevices = [...own?.keys() ?? []].map(deviceFromKey);
|
|
72
|
+
const recentPeer = mostRecentPairing.get(connection);
|
|
73
|
+
let mostRecentDevice;
|
|
74
|
+
if (own && recentPeer) {
|
|
75
|
+
for (const [key, peer] of own) if (peer === recentPeer) {
|
|
76
|
+
mostRecentDevice = deviceFromKey(key);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
device,
|
|
82
|
+
adverts,
|
|
83
|
+
pairedDevices,
|
|
84
|
+
...mostRecentDevice !== void 0 ? { mostRecentDevice } : {}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function restoreConnections(entries) {
|
|
88
|
+
const byDevice = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const { connection, state } of entries) {
|
|
90
|
+
connections.add(connection);
|
|
91
|
+
connectionDevice.set(connection, state.device);
|
|
92
|
+
byDevice.set(deviceKey(state.device), connection);
|
|
93
|
+
for (const advert of state.adverts) devices.set(deviceKey(advert.device), {
|
|
47
94
|
connection,
|
|
48
95
|
advert
|
|
49
96
|
});
|
|
97
|
+
}
|
|
98
|
+
for (const { connection, state } of entries) for (const paired of state.pairedDevices) {
|
|
99
|
+
const peer = byDevice.get(deviceKey(paired));
|
|
100
|
+
if (peer === void 0) continue;
|
|
101
|
+
pairingsOf(connection).set(deviceKey(paired), peer);
|
|
102
|
+
pairingsOf(peer).set(deviceKey(state.device), connection);
|
|
103
|
+
}
|
|
104
|
+
for (const { connection, state } of entries) {
|
|
105
|
+
const recent = state.mostRecentDevice;
|
|
106
|
+
if (recent === void 0) continue;
|
|
107
|
+
const peer = byDevice.get(deviceKey(recent));
|
|
108
|
+
if (peer !== void 0) mostRecentPairing.set(connection, peer);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function handleFrame(connection, frame) {
|
|
112
|
+
if (frame.type === "gossip") {
|
|
113
|
+
const supplanted = /* @__PURE__ */ new Set();
|
|
114
|
+
const accepted = [];
|
|
115
|
+
for (const advert of frame.peers) {
|
|
116
|
+
if (!await verifyPeerAdvert(options.identity, advert)) continue;
|
|
117
|
+
const key = deviceKey(advert.device);
|
|
118
|
+
const previous = devices.get(key);
|
|
119
|
+
if (previous !== void 0 && !supersedes(previous, advert, connection)) continue;
|
|
120
|
+
if (previous !== void 0 && previous.connection !== connection) supplanted.add(previous.connection);
|
|
121
|
+
devices.set(key, {
|
|
122
|
+
connection,
|
|
123
|
+
advert
|
|
124
|
+
});
|
|
125
|
+
accepted.push(advert);
|
|
126
|
+
}
|
|
127
|
+
if (accepted.length === 0) return;
|
|
50
128
|
for (const registration of devices.values()) if (registration.connection === connection) {
|
|
51
129
|
connectionDevice.set(connection, registration.advert.device);
|
|
52
130
|
break;
|
|
53
131
|
}
|
|
132
|
+
stateChanged(connection);
|
|
133
|
+
for (const other of supplanted) stateChanged(other);
|
|
134
|
+
const forwarded = {
|
|
135
|
+
type: "gossip",
|
|
136
|
+
peers: accepted
|
|
137
|
+
};
|
|
54
138
|
for (const other of connections) {
|
|
55
139
|
if (other === connection) continue;
|
|
56
140
|
try {
|
|
57
|
-
await other.send(
|
|
141
|
+
await other.send(forwarded);
|
|
58
142
|
} catch {}
|
|
59
143
|
}
|
|
60
144
|
const catchUp = [];
|
|
@@ -73,6 +157,8 @@ function createRelayHub() {
|
|
|
73
157
|
const initiatorDevice = connectionDevice.get(connection);
|
|
74
158
|
if (initiatorDevice === void 0) return;
|
|
75
159
|
addPairing(connection, initiatorDevice, registration.connection, registration.advert.device);
|
|
160
|
+
stateChanged(connection);
|
|
161
|
+
stateChanged(registration.connection);
|
|
76
162
|
await registration.connection.send({
|
|
77
163
|
type: "relay-inbound",
|
|
78
164
|
"source-device": initiatorDevice
|
|
@@ -115,6 +201,8 @@ function createRelayHub() {
|
|
|
115
201
|
},
|
|
116
202
|
onFrame: handleFrame,
|
|
117
203
|
onDisconnect: forgetConnection,
|
|
204
|
+
exportConnection,
|
|
205
|
+
restoreConnections,
|
|
118
206
|
stop() {
|
|
119
207
|
connections.clear();
|
|
120
208
|
devices.clear();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { N as Frame } from "../protocol-
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
1
|
+
import { N as Frame } from "../protocol-Bi2fzBCc.cjs";
|
|
2
|
+
import { t as Connection } from "../transport-Cq_odHy3.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
|
-
import { N as Frame } from "../protocol-
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
1
|
+
import { N as Frame } from "../protocol-Bi2fzBCc.mjs";
|
|
2
|
+
import { t as Connection } from "../transport-GcJ4LYVE.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,5 +1,5 @@
|
|
|
1
|
-
import { bt as RevocationEntry } from "../protocol-
|
|
2
|
-
import { a as RevocationCheck, d as VerifyRevocationEntryOptions, o as RevocationEntryVerdict } from "../tokens-
|
|
1
|
+
import { bt as RevocationEntry } from "../protocol-Bi2fzBCc.cjs";
|
|
2
|
+
import { a as RevocationCheck, d as VerifyRevocationEntryOptions, o as RevocationEntryVerdict } from "../tokens-BDVxEvl2.cjs";
|
|
3
3
|
//#region src/domain/revocation-view.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* An in-memory RevocationCheck fed by ingested revocation-announce frames. Keyed by token-id alone, per management.cddl's contract: a token-id can legitimately carry multiple recorded entries from different issuers (the token's own issuer, and/or any number of parties holding a delegated manage:revoke authorization over it) -- entriesFor returns every one of them unfiltered, since checking which actually apply (issuer-match, or a verified authorization) is verifyTokenChain's own obligation, not this store's.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { bt as RevocationEntry } from "../protocol-
|
|
2
|
-
import { a as RevocationCheck, d as VerifyRevocationEntryOptions, o as RevocationEntryVerdict } from "../tokens-
|
|
1
|
+
import { bt as RevocationEntry } from "../protocol-Bi2fzBCc.mjs";
|
|
2
|
+
import { a as RevocationCheck, d as VerifyRevocationEntryOptions, o as RevocationEntryVerdict } from "../tokens-0r9ipK1r.mjs";
|
|
3
3
|
//#region src/domain/revocation-view.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* An in-memory RevocationCheck fed by ingested revocation-announce frames. Keyed by token-id alone, per management.cddl's contract: a token-id can legitimately carry multiple recorded entries from different issuers (the token's own issuer, and/or any number of parties holding a delegated manage:revoke authorization over it) -- entriesFor returns every one of them unfiltered, since checking which actually apply (issuer-match, or a verified authorization) is verifyTokenChain's own obligation, not this store's.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-
|
|
2
|
-
import { t as IdentityPort } from "../identity-
|
|
1
|
+
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-Bi2fzBCc.cjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-DImeXZcq.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DiSx-WKM.cjs";
|
|
4
4
|
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.cjs";
|
|
5
|
-
import { a as RevocationCheck } from "../tokens-
|
|
5
|
+
import { a as RevocationCheck } from "../tokens-BDVxEvl2.cjs";
|
|
6
6
|
//#region src/domain/room-rekey.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* Builds a room.rekey command per room.cddl. The outer verb is room:member, the same "one resource, several inner verbs" pattern room.send/room.leave/room.members already establish -- not a new capability of its own.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-
|
|
2
|
-
import { t as IdentityPort } from "../identity-
|
|
1
|
+
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-Bi2fzBCc.mjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-Dc4xmwp3.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DiSx-WKM.mjs";
|
|
4
4
|
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.mjs";
|
|
5
|
-
import { a as RevocationCheck } from "../tokens-
|
|
5
|
+
import { a as RevocationCheck } from "../tokens-0r9ipK1r.mjs";
|
|
6
6
|
//#region src/domain/room-rekey.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* Builds a room.rekey command per room.cddl. The outer verb is room:member, the same "one resource, several inner verbs" pattern room.send/room.leave/room.members already establish -- not a new capability of its own.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as verifyRoomToken, i as VerifyRoomTokenOptions, n as RoomTokenVerdict, r as RoomTokenVerdictReason, t as ROOM_MEMBER_CAPABILITY } from "../room-token-verification-
|
|
1
|
+
import { a as verifyRoomToken, i as VerifyRoomTokenOptions, n as RoomTokenVerdict, r as RoomTokenVerdictReason, t as ROOM_MEMBER_CAPABILITY } from "../room-token-verification-CDXkm_Tg.cjs";
|
|
2
2
|
export { ROOM_MEMBER_CAPABILITY, RoomTokenVerdict, RoomTokenVerdictReason, VerifyRoomTokenOptions, verifyRoomToken };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as verifyRoomToken, i as VerifyRoomTokenOptions, n as RoomTokenVerdict, r as RoomTokenVerdictReason, t as ROOM_MEMBER_CAPABILITY } from "../room-token-verification-
|
|
1
|
+
import { a as verifyRoomToken, i as VerifyRoomTokenOptions, n as RoomTokenVerdict, r as RoomTokenVerdictReason, t as ROOM_MEMBER_CAPABILITY } from "../room-token-verification-CnHNaA8J.mjs";
|
|
2
2
|
export { ROOM_MEMBER_CAPABILITY, RoomTokenVerdict, RoomTokenVerdictReason, VerifyRoomTokenOptions, verifyRoomToken };
|