wire-mesh-core 4.0.8 → 4.1.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/dist/adapters/node-identity.cjs +16 -7
- package/dist/adapters/node-identity.d.cts +1 -1
- package/dist/adapters/node-identity.d.mts +1 -1
- package/dist/adapters/node-identity.mjs +16 -7
- package/dist/adapters/threshold-identity.d.cts +1 -1
- package/dist/adapters/threshold-identity.d.mts +1 -1
- package/dist/domain/capability-grant.d.cts +1 -1
- package/dist/domain/capability-grant.d.mts +1 -1
- package/dist/domain/capability-request.d.cts +1 -1
- package/dist/domain/capability-request.d.mts +1 -1
- package/dist/domain/channel-exhausted-error.cjs +11 -0
- package/dist/domain/channel-exhausted-error.d.cts +6 -0
- package/dist/domain/channel-exhausted-error.d.mts +6 -0
- package/dist/domain/channel-exhausted-error.mjs +10 -0
- package/dist/domain/data-sync.d.cts +1 -1
- package/dist/domain/data-sync.d.mts +1 -1
- package/dist/domain/handshake.cjs +9 -0
- package/dist/domain/handshake.d.cts +2 -0
- package/dist/domain/handshake.d.mts +2 -0
- package/dist/domain/handshake.mjs +9 -1
- package/dist/domain/mesh-session.cjs +112 -48
- package/dist/domain/mesh-session.d.cts +4 -2
- package/dist/domain/mesh-session.d.mts +4 -2
- package/dist/domain/mesh-session.mjs +111 -47
- package/dist/domain/notice-board.d.cts +1 -1
- package/dist/domain/notice-board.d.mts +1 -1
- package/dist/domain/peer-advert.d.cts +1 -1
- package/dist/domain/peer-advert.d.mts +1 -1
- package/dist/domain/room-rekey.d.cts +1 -1
- package/dist/domain/room-rekey.d.mts +1 -1
- package/dist/domain/room.d.cts +1 -1
- package/dist/domain/room.d.mts +1 -1
- package/dist/domain/tokens.cjs +8 -0
- package/dist/domain/tokens.d.cts +4 -2
- package/dist/domain/tokens.d.mts +4 -2
- package/dist/domain/tokens.mjs +9 -1
- package/dist/generated/protocol.cjs +8 -8
- package/dist/generated/protocol.mjs +8 -8
- package/dist/{identity-EV-HHv_D.d.mts → identity-BioGOV9M.d.mts} +10 -1
- package/dist/{identity-B6nhlSOT.d.cts → identity-C5jq53NV.d.cts} +10 -1
- package/dist/ports/identity.cjs +13 -0
- package/dist/ports/identity.d.cts +2 -2
- package/dist/ports/identity.d.mts +2 -2
- package/dist/ports/identity.mjs +12 -1
- package/package.json +5 -1
- package/wasm-dist/wire_mesh_threshold_wasm_bg.wasm +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_ports_identity = require("../ports/identity.cjs");
|
|
2
3
|
let node_crypto = require("node:crypto");
|
|
3
4
|
//#region src/adapters/node-identity.ts
|
|
4
5
|
const ES256 = -7;
|
|
@@ -11,7 +12,7 @@ const EDDSA_PUBLIC_KEY_BYTES = 32;
|
|
|
11
12
|
function publicKeySize(alg) {
|
|
12
13
|
if (alg === ES256) return ES256_PUBLIC_KEY_BYTES;
|
|
13
14
|
if (alg === EDDSA) return EDDSA_PUBLIC_KEY_BYTES;
|
|
14
|
-
throw new
|
|
15
|
+
throw new require_ports_identity.IdentityKeyError("unsupported_alg", `unsupported identity-key alg ${String(alg)}`);
|
|
15
16
|
}
|
|
16
17
|
function algParams(alg) {
|
|
17
18
|
if (alg === ES256) return {
|
|
@@ -19,7 +20,7 @@ function algParams(alg) {
|
|
|
19
20
|
hash: "SHA-256"
|
|
20
21
|
};
|
|
21
22
|
if (alg === EDDSA) return { name: "Ed25519" };
|
|
22
|
-
throw new
|
|
23
|
+
throw new require_ports_identity.IdentityKeyError("unsupported_alg", `unsupported identity-key alg ${String(alg)}`);
|
|
23
24
|
}
|
|
24
25
|
/** Copies into a fresh, non-shared, whole-buffer Uint8Array -- Web Crypto's BufferSource parameters reject a view over a SharedArrayBuffer or a sub-range view, neither of which a caller-supplied Uint8Array is guaranteed not to be. */
|
|
25
26
|
function toBufferSource(bytes) {
|
|
@@ -36,7 +37,7 @@ function importedKeyId(key) {
|
|
|
36
37
|
return `${String(key.alg)}:${Buffer.from(key["public-key"]).toString("base64")}`;
|
|
37
38
|
}
|
|
38
39
|
async function importPublicKey(key) {
|
|
39
|
-
if (key["public-key"].length !== publicKeySize(key.alg)) throw new
|
|
40
|
+
if (key["public-key"].length !== publicKeySize(key.alg)) throw new require_ports_identity.IdentityKeyError("invalid_key", `identity-key public-key is ${String(key["public-key"].length)} bytes, not the ${String(publicKeySize(key.alg))} an alg ${String(key.alg)} key has`);
|
|
40
41
|
const id = importedKeyId(key);
|
|
41
42
|
const held = importedKeys.get(id);
|
|
42
43
|
if (held !== void 0) {
|
|
@@ -53,13 +54,21 @@ async function importPublicKey(key) {
|
|
|
53
54
|
}
|
|
54
55
|
return pending;
|
|
55
56
|
}
|
|
57
|
+
/** Imports the raw public key, reporting a key the algorithm cannot import (not a point on the curve, for ES256) as an IdentityKeyError rather than the Web Crypto error it arrives as. An Ed25519 import does not check that the bytes are a curve point: such a key imports, and every signature under it fails to verify. */
|
|
58
|
+
async function importRawPublicKey(key, algorithm) {
|
|
59
|
+
try {
|
|
60
|
+
return await node_crypto.webcrypto.subtle.importKey("raw", toBufferSource(key["public-key"]), algorithm, false, ["verify"]);
|
|
61
|
+
} catch (cause) {
|
|
62
|
+
throw new require_ports_identity.IdentityKeyError("invalid_key", `identity-key public-key does not import as an alg ${String(key.alg)} key`, { cause });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
56
65
|
async function importPublicKeyUncached(key) {
|
|
57
|
-
if (key.alg === ES256) return
|
|
66
|
+
if (key.alg === ES256) return importRawPublicKey(key, {
|
|
58
67
|
name: "ECDSA",
|
|
59
68
|
namedCurve: "P-256"
|
|
60
|
-
}
|
|
61
|
-
if (key.alg === EDDSA) return
|
|
62
|
-
throw new
|
|
69
|
+
});
|
|
70
|
+
if (key.alg === EDDSA) return importRawPublicKey(key, { name: "Ed25519" });
|
|
71
|
+
throw new require_ports_identity.IdentityKeyError("unsupported_alg", `unsupported identity-key alg ${String(key.alg)}`);
|
|
63
72
|
}
|
|
64
73
|
async function deriveDeviceId(publicKey) {
|
|
65
74
|
return new Uint8Array(await node_crypto.webcrypto.subtle.digest("SHA-256", toBufferSource(publicKey)));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
|
|
3
3
|
import { webcrypto } from "node:crypto";
|
|
4
4
|
//#region src/adapters/node-identity.d.ts
|
|
5
5
|
export declare function deriveDeviceId(publicKey: Uint8Array): Promise<DeviceId>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
|
|
3
3
|
import { webcrypto } from "node:crypto";
|
|
4
4
|
//#region src/adapters/node-identity.d.ts
|
|
5
5
|
export declare function deriveDeviceId(publicKey: Uint8Array): Promise<DeviceId>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IdentityKeyError } from "../ports/identity.mjs";
|
|
1
2
|
import { webcrypto } from "node:crypto";
|
|
2
3
|
//#region src/adapters/node-identity.ts
|
|
3
4
|
const ES256 = -7;
|
|
@@ -10,7 +11,7 @@ const EDDSA_PUBLIC_KEY_BYTES = 32;
|
|
|
10
11
|
function publicKeySize(alg) {
|
|
11
12
|
if (alg === ES256) return ES256_PUBLIC_KEY_BYTES;
|
|
12
13
|
if (alg === EDDSA) return EDDSA_PUBLIC_KEY_BYTES;
|
|
13
|
-
throw new
|
|
14
|
+
throw new IdentityKeyError("unsupported_alg", `unsupported identity-key alg ${String(alg)}`);
|
|
14
15
|
}
|
|
15
16
|
function algParams(alg) {
|
|
16
17
|
if (alg === ES256) return {
|
|
@@ -18,7 +19,7 @@ function algParams(alg) {
|
|
|
18
19
|
hash: "SHA-256"
|
|
19
20
|
};
|
|
20
21
|
if (alg === EDDSA) return { name: "Ed25519" };
|
|
21
|
-
throw new
|
|
22
|
+
throw new IdentityKeyError("unsupported_alg", `unsupported identity-key alg ${String(alg)}`);
|
|
22
23
|
}
|
|
23
24
|
/** Copies into a fresh, non-shared, whole-buffer Uint8Array -- Web Crypto's BufferSource parameters reject a view over a SharedArrayBuffer or a sub-range view, neither of which a caller-supplied Uint8Array is guaranteed not to be. */
|
|
24
25
|
function toBufferSource(bytes) {
|
|
@@ -35,7 +36,7 @@ function importedKeyId(key) {
|
|
|
35
36
|
return `${String(key.alg)}:${Buffer.from(key["public-key"]).toString("base64")}`;
|
|
36
37
|
}
|
|
37
38
|
async function importPublicKey(key) {
|
|
38
|
-
if (key["public-key"].length !== publicKeySize(key.alg)) throw new
|
|
39
|
+
if (key["public-key"].length !== publicKeySize(key.alg)) throw new IdentityKeyError("invalid_key", `identity-key public-key is ${String(key["public-key"].length)} bytes, not the ${String(publicKeySize(key.alg))} an alg ${String(key.alg)} key has`);
|
|
39
40
|
const id = importedKeyId(key);
|
|
40
41
|
const held = importedKeys.get(id);
|
|
41
42
|
if (held !== void 0) {
|
|
@@ -52,13 +53,21 @@ async function importPublicKey(key) {
|
|
|
52
53
|
}
|
|
53
54
|
return pending;
|
|
54
55
|
}
|
|
56
|
+
/** Imports the raw public key, reporting a key the algorithm cannot import (not a point on the curve, for ES256) as an IdentityKeyError rather than the Web Crypto error it arrives as. An Ed25519 import does not check that the bytes are a curve point: such a key imports, and every signature under it fails to verify. */
|
|
57
|
+
async function importRawPublicKey(key, algorithm) {
|
|
58
|
+
try {
|
|
59
|
+
return await webcrypto.subtle.importKey("raw", toBufferSource(key["public-key"]), algorithm, false, ["verify"]);
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
throw new IdentityKeyError("invalid_key", `identity-key public-key does not import as an alg ${String(key.alg)} key`, { cause });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
55
64
|
async function importPublicKeyUncached(key) {
|
|
56
|
-
if (key.alg === ES256) return
|
|
65
|
+
if (key.alg === ES256) return importRawPublicKey(key, {
|
|
57
66
|
name: "ECDSA",
|
|
58
67
|
namedCurve: "P-256"
|
|
59
|
-
}
|
|
60
|
-
if (key.alg === EDDSA) return
|
|
61
|
-
throw new
|
|
68
|
+
});
|
|
69
|
+
if (key.alg === EDDSA) return importRawPublicKey(key, { name: "Ed25519" });
|
|
70
|
+
throw new IdentityKeyError("unsupported_alg", `unsupported identity-key alg ${String(key.alg)}`);
|
|
62
71
|
}
|
|
63
72
|
async function deriveDeviceId(publicKey) {
|
|
64
73
|
return new Uint8Array(await webcrypto.subtle.digest("SHA-256", toBufferSource(publicKey)));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
|
|
3
3
|
import { DeviceKeyed } from "./threshold-wasm.cjs";
|
|
4
4
|
import { ThresholdSubject } from "../domain/threshold-subject.cjs";
|
|
5
5
|
//#region src/adapters/threshold-identity.d.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
|
|
3
3
|
import { DeviceKeyed } from "./threshold-wasm.mjs";
|
|
4
4
|
import { ThresholdSubject } from "../domain/threshold-subject.mjs";
|
|
5
5
|
//#region src/adapters/threshold-identity.d.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
4
|
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.cjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.cjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
4
|
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.mjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken, u as CapabilityGrantOk } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
4
|
import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
|
|
5
5
|
import { m as TokenDelegateHandler, r as ConditionsContext } from "../token-predicates-lg4TtUxm.cjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken, u as CapabilityGrantOk } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
4
|
import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
|
|
5
5
|
import { m as TokenDelegateHandler, r as ConditionsContext } from "../token-predicates-CrieP1WI.mjs";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/domain/channel-exhausted-error.ts
|
|
3
|
+
/** Thrown when a secure channel's send counter is spent: nothing more can be sealed on that channel, and a new handshake gives a fresh counter. The session forgets such a channel, so the next send to the peer starts one; a caller that sees this error from a send can retry it. */
|
|
4
|
+
var ChannelExhaustedError = class extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("the channel's send counter is exhausted");
|
|
7
|
+
this.name = "ChannelExhaustedError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
exports.ChannelExhaustedError = ChannelExhaustedError;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/domain/channel-exhausted-error.d.ts
|
|
2
|
+
/** Thrown when a secure channel's send counter is spent: nothing more can be sealed on that channel, and a new handshake gives a fresh counter. The session forgets such a channel, so the next send to the peer starts one; a caller that sees this error from a send can retry it. */
|
|
3
|
+
export declare class ChannelExhaustedError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/domain/channel-exhausted-error.d.ts
|
|
2
|
+
/** Thrown when a secure channel's send counter is spent: nothing more can be sealed on that channel, and a new handshake gives a fresh counter. The session forgets such a channel, so the next send to the peer starts one; a caller that sees this error from a send can retry it. */
|
|
3
|
+
export declare class ChannelExhaustedError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/domain/channel-exhausted-error.ts
|
|
2
|
+
/** Thrown when a secure channel's send counter is spent: nothing more can be sealed on that channel, and a new handshake gives a fresh counter. The session forgets such a channel, so the next send to the peer starts one; a caller that sees this error from a send can retry it. */
|
|
3
|
+
var ChannelExhaustedError = class extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super("the channel's send counter is exhausted");
|
|
6
|
+
this.name = "ChannelExhaustedError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { ChannelExhaustedError };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DzmpxxUr.cjs";
|
|
2
2
|
import { t as KeyValueStorage } from "../storage-jbGFAYIV.cjs";
|
|
3
|
-
import {
|
|
3
|
+
import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
|
|
4
4
|
//#region src/domain/data-sync.d.ts
|
|
5
5
|
export interface DataSyncOptions {
|
|
6
6
|
identity: IdentityPort;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DzmpxxUr.mjs";
|
|
2
2
|
import { t as KeyValueStorage } from "../storage-jbGFAYIV.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
|
|
4
4
|
//#region src/domain/data-sync.d.ts
|
|
5
5
|
export interface DataSyncOptions {
|
|
6
6
|
identity: IdentityPort;
|
|
@@ -2,6 +2,14 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
//#region src/domain/handshake.ts
|
|
3
3
|
/** The highest protocol version this build of core understands. */
|
|
4
4
|
const SUPPORTED_PROTOCOL_VERSION = 1;
|
|
5
|
+
/** The handshake this side sends: the version it supports and the domains it offers. */
|
|
6
|
+
function localHandshake(domains) {
|
|
7
|
+
return {
|
|
8
|
+
type: "handshake",
|
|
9
|
+
version: 1,
|
|
10
|
+
domains: [...domains]
|
|
11
|
+
};
|
|
12
|
+
}
|
|
5
13
|
/**
|
|
6
14
|
* Domain names that must never be negotiated, per handshake.cddl: core/federation is retired (its string stays reserved, never reused) and "a peer must never advertise or negotiate it". Excluded here even if both peers advertise it -- two buggy advertisers must not end up speaking a dead domain.
|
|
7
15
|
*/
|
|
@@ -20,4 +28,5 @@ function negotiate(local, remote) {
|
|
|
20
28
|
}
|
|
21
29
|
//#endregion
|
|
22
30
|
exports.SUPPORTED_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSION;
|
|
31
|
+
exports.localHandshake = localHandshake;
|
|
23
32
|
exports.negotiate = negotiate;
|
|
@@ -2,6 +2,8 @@ import { A as DomainId, R as HandshakeFrame, lt as ProtocolVersion } from "../pr
|
|
|
2
2
|
//#region src/domain/handshake.d.ts
|
|
3
3
|
/** The highest protocol version this build of core understands. */
|
|
4
4
|
export declare const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion;
|
|
5
|
+
/** The handshake this side sends: the version it supports and the domains it offers. */
|
|
6
|
+
export declare function localHandshake(domains: readonly string[]): HandshakeFrame;
|
|
5
7
|
export interface NegotiationResult {
|
|
6
8
|
ok: boolean;
|
|
7
9
|
/** The version both peers will speak for the rest of the session -- the lower of the two offered versions, so a peer never has to understand a frame shape it didn't advertise. */
|
|
@@ -2,6 +2,8 @@ import { A as DomainId, R as HandshakeFrame, lt as ProtocolVersion } from "../pr
|
|
|
2
2
|
//#region src/domain/handshake.d.ts
|
|
3
3
|
/** The highest protocol version this build of core understands. */
|
|
4
4
|
export declare const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion;
|
|
5
|
+
/** The handshake this side sends: the version it supports and the domains it offers. */
|
|
6
|
+
export declare function localHandshake(domains: readonly string[]): HandshakeFrame;
|
|
5
7
|
export interface NegotiationResult {
|
|
6
8
|
ok: boolean;
|
|
7
9
|
/** The version both peers will speak for the rest of the session -- the lower of the two offered versions, so a peer never has to understand a frame shape it didn't advertise. */
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
//#region src/domain/handshake.ts
|
|
2
2
|
/** The highest protocol version this build of core understands. */
|
|
3
3
|
const SUPPORTED_PROTOCOL_VERSION = 1;
|
|
4
|
+
/** The handshake this side sends: the version it supports and the domains it offers. */
|
|
5
|
+
function localHandshake(domains) {
|
|
6
|
+
return {
|
|
7
|
+
type: "handshake",
|
|
8
|
+
version: 1,
|
|
9
|
+
domains: [...domains]
|
|
10
|
+
};
|
|
11
|
+
}
|
|
4
12
|
/**
|
|
5
13
|
* Domain names that must never be negotiated, per handshake.cddl: core/federation is retired (its string stays reserved, never reused) and "a peer must never advertise or negotiate it". Excluded here even if both peers advertise it -- two buggy advertisers must not end up speaking a dead domain.
|
|
6
14
|
*/
|
|
@@ -18,4 +26,4 @@ function negotiate(local, remote) {
|
|
|
18
26
|
};
|
|
19
27
|
}
|
|
20
28
|
//#endregion
|
|
21
|
-
export { SUPPORTED_PROTOCOL_VERSION, negotiate };
|
|
29
|
+
export { SUPPORTED_PROTOCOL_VERSION, localHandshake, negotiate };
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_adapters_frame_codec = require("../adapters/frame-codec.cjs");
|
|
3
|
+
const require_ports_identity = require("../ports/identity.cjs");
|
|
3
4
|
const require_domain_device_id = require("./device-id.cjs");
|
|
4
5
|
const require_domain_token_scope = require("./token-scope.cjs");
|
|
5
6
|
const require_gossip_extensions = require("../gossip-extensions-BBYgMZ2N.cjs");
|
|
6
7
|
const require_domain_peer_advert = require("./peer-advert.cjs");
|
|
7
8
|
const require_domain_async_queue = require("./async-queue.cjs");
|
|
9
|
+
const require_domain_channel_exhausted_error = require("./channel-exhausted-error.cjs");
|
|
8
10
|
const require_domain_handshake = require("./handshake.cjs");
|
|
9
11
|
const require_domain_hub_mailbox = require("./hub-mailbox.cjs");
|
|
10
12
|
const require_domain_ping_round_trips = require("./ping-round-trips.cjs");
|
|
@@ -27,6 +29,19 @@ function createRelayPairings() {
|
|
|
27
29
|
};
|
|
28
30
|
}
|
|
29
31
|
//#endregion
|
|
32
|
+
//#region src/domain/bounded-log.ts
|
|
33
|
+
/** A log that keeps only its most recent `window` entries: newest at the end, the oldest dropped once the window is full. Pure and closure-free, split out of mesh-session.ts to keep that file under this repo's max-lines lint budget; nothing here is session-shaped. */
|
|
34
|
+
function createBoundedLog(window) {
|
|
35
|
+
const log = [];
|
|
36
|
+
return {
|
|
37
|
+
record: (entry) => {
|
|
38
|
+
log.push(entry);
|
|
39
|
+
if (log.length > window) log.splice(0, log.length - window);
|
|
40
|
+
},
|
|
41
|
+
entries: () => [...log]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
30
45
|
//#region src/domain/secure-channel.ts
|
|
31
46
|
/**
|
|
32
47
|
* The end-to-end channel two devices open between themselves inside a relay pairing (spec/secure-channel.cddl). A relay only forwards the bytes of a `relay-data` frame, and without this it could read them, alter them, and say who sent them. Here the two devices authenticate each other's identity keys over a fresh ECDH exchange, derive one AES-256-GCM key per direction, and seal every frame they exchange so the relay sees ciphertext and every receiver learns the sender's identity from the handshake and nothing a relay asserts.
|
|
@@ -96,16 +111,16 @@ async function beginHandshake(identity, peer) {
|
|
|
96
111
|
};
|
|
97
112
|
}
|
|
98
113
|
/**
|
|
99
|
-
* Checks a received hello:
|
|
114
|
+
* Checks a received hello: it must be addressed to this device, its identity key must be a usable key, and its signature must verify under that key over the input spec/secure-channel.cddl defines. A hostile hello gets a verdict. An error from the identity port that is not an IdentityKeyError is a failure of the port, not a verdict about the peer, and propagates.
|
|
100
115
|
*/
|
|
101
116
|
async function verifyHello(identity, hello) {
|
|
102
117
|
const key = hello["identity-key"];
|
|
118
|
+
const sender = await identity.deriveDeviceId(key["public-key"]);
|
|
119
|
+
if (!require_domain_token_scope.bytesEqual(hello["to-device"], identity.deviceId)) return {
|
|
120
|
+
ok: false,
|
|
121
|
+
reason: "wrong_recipient"
|
|
122
|
+
};
|
|
103
123
|
try {
|
|
104
|
-
const sender = await identity.deriveDeviceId(key["public-key"]);
|
|
105
|
-
if (!require_domain_token_scope.bytesEqual(hello["to-device"], identity.deviceId)) return {
|
|
106
|
-
ok: false,
|
|
107
|
-
reason: "wrong_recipient"
|
|
108
|
-
};
|
|
109
124
|
return await identity.verify(key, helloSigningInput(hello["ephemeral-key"], hello.nonce, sender, identity.deviceId), hello.signature) ? {
|
|
110
125
|
ok: true,
|
|
111
126
|
peer: {
|
|
@@ -116,13 +131,30 @@ async function verifyHello(identity, hello) {
|
|
|
116
131
|
ok: false,
|
|
117
132
|
reason: "bad_signature"
|
|
118
133
|
};
|
|
119
|
-
} catch {
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (!(error instanceof require_ports_identity.IdentityKeyError)) throw error;
|
|
120
136
|
return {
|
|
121
137
|
ok: false,
|
|
122
|
-
reason: "
|
|
138
|
+
reason: error.fault === "unsupported_alg" ? "unsupported_identity_alg" : "invalid_identity_key"
|
|
123
139
|
};
|
|
124
140
|
}
|
|
125
141
|
}
|
|
142
|
+
/** The largest counter a channel uses: the counter is an unsigned 64-bit integer on the wire, and this implementation holds it as a JavaScript number, exact up to this value. A frame above it is refused at decoding, never truncated, and sealing refuses to go past it, never wraps. */
|
|
143
|
+
const MAX_COUNTER = Number.MAX_SAFE_INTEGER;
|
|
144
|
+
/** Throws a RangeError unless `counter` is a counter a channel can start from: a non-negative safe integer. */
|
|
145
|
+
function assertFirstSendCounter(counter) {
|
|
146
|
+
if (!Number.isSafeInteger(counter) || counter < 0) throw new RangeError("firstSendCounter must be a non-negative safe integer");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Takes the next send counter from `next`, the counter the next seal would use: returns it with the counter that follows. Throws once `next` is past MAX_COUNTER, since a counter that cannot be held exactly would repeat an IV under the same key.
|
|
150
|
+
*/
|
|
151
|
+
function claimSendCounter(next) {
|
|
152
|
+
if (next > MAX_COUNTER) throw new require_domain_channel_exhausted_error.ChannelExhaustedError();
|
|
153
|
+
return {
|
|
154
|
+
counter: next,
|
|
155
|
+
next: next + 1
|
|
156
|
+
};
|
|
157
|
+
}
|
|
126
158
|
function ivFor(counter) {
|
|
127
159
|
const iv = new Uint8Array(GCM_IV_BYTE_LENGTH);
|
|
128
160
|
iv.set(counterBytes(counter), IV_COUNTER_OFFSET);
|
|
@@ -144,11 +176,10 @@ async function deriveDirectionKey({ secret, salt, low, high, label }) {
|
|
|
144
176
|
}, false, ["encrypt", "decrypt"]);
|
|
145
177
|
}
|
|
146
178
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* `peer` is the device verifyHello authenticated for `remote`, and `pending` is this side's own half, whether the handshake was begun here or in answer to that hello.
|
|
179
|
+
* establishChannel with its inputs in one object, and the one extra that tests need: where this end's send counter starts. This module is not a published entry (it is in tsdown.config.ts's INTERNAL_MODULES) and the session never passes `firstSendCounter`, so every real channel starts at 0; a test passes a counter near MAX_COUNTER to reach the bound without sealing 2^53 frames.
|
|
150
180
|
*/
|
|
151
|
-
async function
|
|
181
|
+
async function establishChannelFrom(identity, { pending, remote, peer, firstSendCounter = 0 }) {
|
|
182
|
+
assertFirstSendCounter(firstSendCounter);
|
|
152
183
|
let secret;
|
|
153
184
|
try {
|
|
154
185
|
const remoteKey = await crypto.subtle.importKey("raw", Uint8Array.from(remote["ephemeral-key"]), ECDH_PARAMS, false, []);
|
|
@@ -179,7 +210,7 @@ async function establishChannel(identity, pending, remote, peer) {
|
|
|
179
210
|
});
|
|
180
211
|
const sendKey = localIsLow ? lowToHigh : highToLow;
|
|
181
212
|
const receiveKey = localIsLow ? highToLow : lowToHigh;
|
|
182
|
-
let nextSendCounter =
|
|
213
|
+
let nextSendCounter = firstSendCounter;
|
|
183
214
|
let highestReceived = -1;
|
|
184
215
|
let sealing = Promise.resolve();
|
|
185
216
|
let opening = Promise.resolve();
|
|
@@ -195,7 +226,10 @@ async function establishChannel(identity, pending, remote, peer) {
|
|
|
195
226
|
};
|
|
196
227
|
}
|
|
197
228
|
async function openOne(data) {
|
|
198
|
-
if (data.counter <= highestReceived) return
|
|
229
|
+
if (data.counter <= highestReceived) return {
|
|
230
|
+
ok: false,
|
|
231
|
+
reason: "replayed"
|
|
232
|
+
};
|
|
199
233
|
let plaintext;
|
|
200
234
|
try {
|
|
201
235
|
plaintext = new Uint8Array(await crypto.subtle.decrypt({
|
|
@@ -204,30 +238,47 @@ async function establishChannel(identity, pending, remote, peer) {
|
|
|
204
238
|
additionalData: associatedData(peer.deviceId, identity.deviceId, data.counter)
|
|
205
239
|
}, receiveKey, Uint8Array.from(data.ciphertext)));
|
|
206
240
|
} catch {
|
|
207
|
-
return
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
reason: "unauthentic"
|
|
244
|
+
};
|
|
208
245
|
}
|
|
209
246
|
highestReceived = Math.max(highestReceived, data.counter);
|
|
210
|
-
|
|
247
|
+
const frame = require_adapters_frame_codec.tryDecodeFrame(plaintext);
|
|
248
|
+
return frame === null ? {
|
|
249
|
+
ok: false,
|
|
250
|
+
reason: "undecodable"
|
|
251
|
+
} : {
|
|
252
|
+
ok: true,
|
|
253
|
+
frame
|
|
254
|
+
};
|
|
211
255
|
}
|
|
256
|
+
const openOutcome = async (data) => {
|
|
257
|
+
const opened = opening.then(async () => openOne(data));
|
|
258
|
+
opening = opened.catch(() => void 0);
|
|
259
|
+
return opened;
|
|
260
|
+
};
|
|
212
261
|
return {
|
|
213
262
|
peer,
|
|
214
263
|
seal: async (frame) => {
|
|
215
|
-
const
|
|
216
|
-
|
|
264
|
+
const claimed = claimSendCounter(nextSendCounter);
|
|
265
|
+
const counter = claimed.counter;
|
|
266
|
+
nextSendCounter = claimed.next;
|
|
217
267
|
const sealed = sealing.then(async () => sealAt(frame, counter));
|
|
218
268
|
sealing = sealed.catch(() => void 0);
|
|
219
269
|
return sealed;
|
|
220
270
|
},
|
|
221
271
|
open: async (data) => {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
272
|
+
const outcome = await openOutcome(data);
|
|
273
|
+
return outcome.ok ? outcome.frame : void 0;
|
|
274
|
+
},
|
|
275
|
+
openOutcome
|
|
226
276
|
};
|
|
227
277
|
}
|
|
228
278
|
//#endregion
|
|
229
279
|
//#region src/domain/relay-channels.ts
|
|
230
|
-
function createRelayChannels(identity, sendHello) {
|
|
280
|
+
function createRelayChannels(identity, sendHello, options = {}) {
|
|
281
|
+
if (options.firstSendCounter !== void 0) assertFirstSendCounter(options.firstSendCounter);
|
|
231
282
|
const channels = /* @__PURE__ */ new Map();
|
|
232
283
|
/** Registers the peer as awaiting at once, so a hello arriving meanwhile completes this handshake instead of starting a second, then sends this side's hello. */
|
|
233
284
|
function start(peer) {
|
|
@@ -253,17 +304,40 @@ function createRelayChannels(identity, sendHello) {
|
|
|
253
304
|
return (existing ?? start(peer)).settle.promise;
|
|
254
305
|
},
|
|
255
306
|
applyHello: async (hello) => {
|
|
256
|
-
|
|
307
|
+
let verdict;
|
|
308
|
+
try {
|
|
309
|
+
verdict = await verifyHello(identity, hello);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
options.onFault?.(error);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
257
314
|
if (!verdict.ok) return;
|
|
258
315
|
const key = require_domain_device_id.deviceIdToHex(verdict.peer.deviceId);
|
|
259
316
|
const current = channels.get(key);
|
|
260
317
|
const entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId);
|
|
261
|
-
const
|
|
262
|
-
|
|
318
|
+
const established = await establishChannelFrom(identity, {
|
|
319
|
+
pending: await entry.pending,
|
|
320
|
+
remote: hello,
|
|
321
|
+
peer: verdict.peer,
|
|
322
|
+
...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
|
|
323
|
+
});
|
|
324
|
+
if (established === void 0) {
|
|
263
325
|
entry.settle.reject(/* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
|
|
264
326
|
if (channels.get(key) === entry) channels.delete(key);
|
|
265
327
|
return;
|
|
266
328
|
}
|
|
329
|
+
const channel = {
|
|
330
|
+
...established,
|
|
331
|
+
seal: async (frame) => {
|
|
332
|
+
try {
|
|
333
|
+
return await established.seal(frame);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
const held = channels.get(key);
|
|
336
|
+
if (error instanceof require_domain_channel_exhausted_error.ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
267
341
|
channels.set(key, {
|
|
268
342
|
status: "established",
|
|
269
343
|
channel
|
|
@@ -306,27 +380,16 @@ const HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
|
306
380
|
* always-on node grows without limit.
|
|
307
381
|
*/
|
|
308
382
|
const FRAME_LOG_WINDOW = 256;
|
|
309
|
-
function localHandshake(domains) {
|
|
310
|
-
return {
|
|
311
|
-
type: "handshake",
|
|
312
|
-
version: 1,
|
|
313
|
-
domains: [...domains]
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
383
|
/**
|
|
317
384
|
* Builds the connection-agnostic session state machine and its public MeshSession surface. dial is null for a session that can never (re)connect on its own -- acceptMeshSession's case, where the one connection it will ever have already exists by construction and reconnect therefore cannot apply (only the remote redialing, and being accepted again, produces a fresh connection). createMeshSession supplies dial as transport.connect so its own connect()/reconnect behaviour is unchanged from before this was factored out.
|
|
318
385
|
*/
|
|
319
|
-
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = [], onFrame, onSessionEnd) {
|
|
386
|
+
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = [], onFrame, onSessionEnd, onLocalFault) {
|
|
320
387
|
let connection = null;
|
|
321
388
|
let state = { status: "idle" };
|
|
322
389
|
let handshake = { status: "pending" };
|
|
323
390
|
const directory = /* @__PURE__ */ new Map();
|
|
324
|
-
const frameLog =
|
|
325
|
-
|
|
326
|
-
function recordFrame(entry) {
|
|
327
|
-
frameLog.push(entry);
|
|
328
|
-
if (frameLog.length > 256) frameLog.splice(0, frameLog.length - 256);
|
|
329
|
-
}
|
|
391
|
+
const frameLog = createBoundedLog(256);
|
|
392
|
+
const recordFrame = frameLog.record;
|
|
330
393
|
let feedCancelled = false;
|
|
331
394
|
const eventQueue = require_domain_async_queue.createAsyncQueue();
|
|
332
395
|
let handshakeTimer = null;
|
|
@@ -341,7 +404,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
341
404
|
getRelayedDevices: () => relayPairings.list()
|
|
342
405
|
});
|
|
343
406
|
const pendingManageRequests = /* @__PURE__ */ new Map();
|
|
344
|
-
const
|
|
407
|
+
const sendHello = async (peer, hello) => {
|
|
345
408
|
if (connection === null) throw new Error("not connected");
|
|
346
409
|
recordFrame({
|
|
347
410
|
direction: "sent",
|
|
@@ -349,7 +412,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
349
412
|
});
|
|
350
413
|
await connection.send(require_adapters_frame_codec.wrapRelayData(hello, peer));
|
|
351
414
|
emit();
|
|
352
|
-
}
|
|
415
|
+
};
|
|
416
|
+
const relayChannels = createRelayChannels(identity, sendHello, { onFault: onLocalFault });
|
|
353
417
|
const incomingQueue = require_domain_async_queue.createAsyncQueue();
|
|
354
418
|
const revocationQueue = require_domain_async_queue.createAsyncQueue();
|
|
355
419
|
const coordinatorQueue = require_domain_async_queue.createAsyncQueue();
|
|
@@ -359,7 +423,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
359
423
|
return {
|
|
360
424
|
state,
|
|
361
425
|
directory: [...directory.values()],
|
|
362
|
-
frameLog:
|
|
426
|
+
frameLog: frameLog.entries()
|
|
363
427
|
};
|
|
364
428
|
}
|
|
365
429
|
function emit() {
|
|
@@ -434,7 +498,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
434
498
|
}
|
|
435
499
|
};
|
|
436
500
|
if (require_gossip_extensions.isVersionGetCommand(frame.command.params)) {
|
|
437
|
-
incoming.respond(require_gossip_extensions.versionGetOutcome());
|
|
501
|
+
incoming.respond(require_gossip_extensions.versionGetOutcome()).catch(() => void 0);
|
|
438
502
|
return;
|
|
439
503
|
}
|
|
440
504
|
incomingQueue.push(incoming);
|
|
@@ -528,7 +592,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
528
592
|
relayPairings.add(targetDevice);
|
|
529
593
|
emit();
|
|
530
594
|
}
|
|
531
|
-
let localHandshakeSent = localHandshake([]);
|
|
595
|
+
let localHandshakeSent = require_domain_handshake.localHandshake([]);
|
|
532
596
|
function applyRemoteHandshake(remote) {
|
|
533
597
|
if (handshake.status !== "pending") return;
|
|
534
598
|
if (handshakeTimer !== null) {
|
|
@@ -618,7 +682,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
618
682
|
connection = link;
|
|
619
683
|
relayChannels.reset("the connection changed before the channel was ready");
|
|
620
684
|
relayPairings.clear();
|
|
621
|
-
localHandshakeSent = localHandshake(localDomains);
|
|
685
|
+
localHandshakeSent = require_domain_handshake.localHandshake(localDomains);
|
|
622
686
|
handshake = { status: "pending" };
|
|
623
687
|
state = {
|
|
624
688
|
status: "connected",
|
|
@@ -828,8 +892,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
828
892
|
}
|
|
829
893
|
};
|
|
830
894
|
}
|
|
831
|
-
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert, onFrame) {
|
|
832
|
-
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses, onFrame);
|
|
895
|
+
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert, onFrame, onLocalFault) {
|
|
896
|
+
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses, onFrame, void 0, onLocalFault);
|
|
833
897
|
return session;
|
|
834
898
|
}
|
|
835
899
|
/** Wires an already-accepted Connection up as a full MeshSession, mirroring exactly what createMeshSession's own dial path does once a connection exists (send handshake, send self-advert, negotiate, consume frames) -- the wire-mesh#45 prerequisite agent-comms needs, since its peers both listen and dial rather than only ever dialing the way web-console's own console UI does. Reconnect does not apply here: if this connection drops, only the remote redialing and being accepted again produces a new connection, and therefore a new session -- there is nothing on this side to retry. */
|