wire-mesh-core 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/domain/device-id.cjs +10 -4
- package/dist/domain/device-id.d.cts +2 -0
- package/dist/domain/device-id.d.mts +2 -0
- package/dist/domain/device-id.mjs +10 -5
- package/dist/domain/mesh-session.cjs +5 -5
- package/dist/domain/mesh-session.d.cts +5 -1
- package/dist/domain/mesh-session.d.mts +5 -1
- package/dist/domain/mesh-session.mjs +5 -5
- package/package.json +1 -1
|
@@ -14,14 +14,20 @@ function bytesToHex(bytes) {
|
|
|
14
14
|
function deviceIdToHex(device) {
|
|
15
15
|
return bytesToHex(device);
|
|
16
16
|
}
|
|
17
|
+
/** The arbitrary-length reverse of bytesToHex -- decodes a lowercase, byte-exact hex string back into the bytes it encodes. Throws on an odd-length string, a non-hex character, or a non-lowercase one, rather than silently truncating or normalising a malformed input, matching deviceIdFromHex's own fail-closed convention for the fixed-length case. */
|
|
18
|
+
function bytesFromHex(hex) {
|
|
19
|
+
if (!/^([0-9a-f]{2})*$/.test(hex)) throw new Error(`expected an even-length, lowercase hex string, got ${JSON.stringify(hex)}`);
|
|
20
|
+
const bytes = new Uint8Array(hex.length / HEX_BYTE_WIDTH);
|
|
21
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = Number.parseInt(hex.slice(i * HEX_BYTE_WIDTH, (i + 1) * HEX_BYTE_WIDTH), HEX_RADIX);
|
|
22
|
+
return bytes;
|
|
23
|
+
}
|
|
17
24
|
/** Parses a lowercase, 64-character device-id-hex string back into the 32-byte DeviceId it encodes. Throws on anything that isn't exactly that shape, rather than silently truncating or zero-padding a malformed input. */
|
|
18
25
|
function deviceIdFromHex(hex) {
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
for (let i = 0; i < bytes.length; i++) bytes[i] = Number.parseInt(hex.slice(i * HEX_BYTE_WIDTH, (i + 1) * HEX_BYTE_WIDTH), HEX_RADIX);
|
|
22
|
-
return require_generated_protocol.deviceIdSchema.parse(bytes);
|
|
26
|
+
if (hex.length !== DEVICE_ID_HEX_LENGTH) throw new Error(`expected a 64-character lowercase hex string, got ${JSON.stringify(hex)}`);
|
|
27
|
+
return require_generated_protocol.deviceIdSchema.parse(bytesFromHex(hex));
|
|
23
28
|
}
|
|
24
29
|
//#endregion
|
|
30
|
+
exports.bytesFromHex = bytesFromHex;
|
|
25
31
|
exports.bytesToHex = bytesToHex;
|
|
26
32
|
exports.deviceIdFromHex = deviceIdFromHex;
|
|
27
33
|
exports.deviceIdToHex = deviceIdToHex;
|
|
@@ -4,6 +4,8 @@ import { v as DeviceId } from "../protocol-B26-5VX7.cjs";
|
|
|
4
4
|
export declare function bytesToHex(bytes: Uint8Array): string;
|
|
5
5
|
/** Lowercase, byte-exact hex -- the same encoding room.cddl's device-id-hex regex and the conformance vectors' synthetic device-ids already use. */
|
|
6
6
|
export declare function deviceIdToHex(device: DeviceId): string;
|
|
7
|
+
/** The arbitrary-length reverse of bytesToHex -- decodes a lowercase, byte-exact hex string back into the bytes it encodes. Throws on an odd-length string, a non-hex character, or a non-lowercase one, rather than silently truncating or normalising a malformed input, matching deviceIdFromHex's own fail-closed convention for the fixed-length case. */
|
|
8
|
+
export declare function bytesFromHex(hex: string): Uint8Array;
|
|
7
9
|
/** Parses a lowercase, 64-character device-id-hex string back into the 32-byte DeviceId it encodes. Throws on anything that isn't exactly that shape, rather than silently truncating or zero-padding a malformed input. */
|
|
8
10
|
export declare function deviceIdFromHex(hex: string): DeviceId;
|
|
9
11
|
//#endregion
|
|
@@ -4,6 +4,8 @@ import { v as DeviceId } from "../protocol-B26-5VX7.mjs";
|
|
|
4
4
|
export declare function bytesToHex(bytes: Uint8Array): string;
|
|
5
5
|
/** Lowercase, byte-exact hex -- the same encoding room.cddl's device-id-hex regex and the conformance vectors' synthetic device-ids already use. */
|
|
6
6
|
export declare function deviceIdToHex(device: DeviceId): string;
|
|
7
|
+
/** The arbitrary-length reverse of bytesToHex -- decodes a lowercase, byte-exact hex string back into the bytes it encodes. Throws on an odd-length string, a non-hex character, or a non-lowercase one, rather than silently truncating or normalising a malformed input, matching deviceIdFromHex's own fail-closed convention for the fixed-length case. */
|
|
8
|
+
export declare function bytesFromHex(hex: string): Uint8Array;
|
|
7
9
|
/** Parses a lowercase, 64-character device-id-hex string back into the 32-byte DeviceId it encodes. Throws on anything that isn't exactly that shape, rather than silently truncating or zero-padding a malformed input. */
|
|
8
10
|
export declare function deviceIdFromHex(hex: string): DeviceId;
|
|
9
11
|
//#endregion
|
|
@@ -13,12 +13,17 @@ function bytesToHex(bytes) {
|
|
|
13
13
|
function deviceIdToHex(device) {
|
|
14
14
|
return bytesToHex(device);
|
|
15
15
|
}
|
|
16
|
+
/** The arbitrary-length reverse of bytesToHex -- decodes a lowercase, byte-exact hex string back into the bytes it encodes. Throws on an odd-length string, a non-hex character, or a non-lowercase one, rather than silently truncating or normalising a malformed input, matching deviceIdFromHex's own fail-closed convention for the fixed-length case. */
|
|
17
|
+
function bytesFromHex(hex) {
|
|
18
|
+
if (!/^([0-9a-f]{2})*$/.test(hex)) throw new Error(`expected an even-length, lowercase hex string, got ${JSON.stringify(hex)}`);
|
|
19
|
+
const bytes = new Uint8Array(hex.length / HEX_BYTE_WIDTH);
|
|
20
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = Number.parseInt(hex.slice(i * HEX_BYTE_WIDTH, (i + 1) * HEX_BYTE_WIDTH), HEX_RADIX);
|
|
21
|
+
return bytes;
|
|
22
|
+
}
|
|
16
23
|
/** Parses a lowercase, 64-character device-id-hex string back into the 32-byte DeviceId it encodes. Throws on anything that isn't exactly that shape, rather than silently truncating or zero-padding a malformed input. */
|
|
17
24
|
function deviceIdFromHex(hex) {
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
for (let i = 0; i < bytes.length; i++) bytes[i] = Number.parseInt(hex.slice(i * HEX_BYTE_WIDTH, (i + 1) * HEX_BYTE_WIDTH), HEX_RADIX);
|
|
21
|
-
return deviceIdSchema.parse(bytes);
|
|
25
|
+
if (hex.length !== DEVICE_ID_HEX_LENGTH) throw new Error(`expected a 64-character lowercase hex string, got ${JSON.stringify(hex)}`);
|
|
26
|
+
return deviceIdSchema.parse(bytesFromHex(hex));
|
|
22
27
|
}
|
|
23
28
|
//#endregion
|
|
24
|
-
export { bytesToHex, deviceIdFromHex, deviceIdToHex };
|
|
29
|
+
export { bytesFromHex, bytesToHex, deviceIdFromHex, deviceIdToHex };
|
|
@@ -16,7 +16,7 @@ function localHandshake(domains) {
|
|
|
16
16
|
/**
|
|
17
17
|
* 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.
|
|
18
18
|
*/
|
|
19
|
-
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert) {
|
|
19
|
+
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = []) {
|
|
20
20
|
let connection = null;
|
|
21
21
|
let state = { status: "idle" };
|
|
22
22
|
let handshake = { status: "pending" };
|
|
@@ -248,7 +248,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert) {
|
|
|
248
248
|
type: "gossip",
|
|
249
249
|
peers: [{
|
|
250
250
|
device: identity.deviceId,
|
|
251
|
-
addresses: [],
|
|
251
|
+
addresses: [...addresses],
|
|
252
252
|
"snapshot-seconds": Math.floor(clock.now() / MS_PER_SECOND)
|
|
253
253
|
}]
|
|
254
254
|
};
|
|
@@ -410,8 +410,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert) {
|
|
|
410
410
|
}
|
|
411
411
|
};
|
|
412
412
|
}
|
|
413
|
-
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null) {
|
|
414
|
-
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address));
|
|
413
|
+
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = []) {
|
|
414
|
+
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), void 0, addresses);
|
|
415
415
|
return session;
|
|
416
416
|
}
|
|
417
417
|
/** 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. */
|
|
@@ -426,7 +426,7 @@ async function acceptMeshSession(connection, identity, localDomains, options = {
|
|
|
426
426
|
if (peerDeviceIdResolved) return;
|
|
427
427
|
peerDeviceIdResolved = true;
|
|
428
428
|
resolvePeerDeviceId?.(advert.device);
|
|
429
|
-
});
|
|
429
|
+
}, options.addresses);
|
|
430
430
|
await wireUpConnection(connection, options.label ?? "accepted", localDomains);
|
|
431
431
|
return {
|
|
432
432
|
...session,
|
|
@@ -85,7 +85,9 @@ export interface MeshSession {
|
|
|
85
85
|
sendManageRequest: (command: ManageCommand, scope: Readonly<CapabilityScope>, targetDevice?: DeviceId, token?: CapabilityToken) => Promise<ManageOutcome>;
|
|
86
86
|
close: () => Promise<void>;
|
|
87
87
|
}
|
|
88
|
-
export declare function createMeshSession(transport: Readonly<Transport>, identity: Readonly<IdentityPort>, clock?: Readonly<Clock>, reconnect?: ReconnectPolicy | null
|
|
88
|
+
export declare function createMeshSession(transport: Readonly<Transport>, identity: Readonly<IdentityPort>, clock?: Readonly<Clock>, reconnect?: ReconnectPolicy | null,
|
|
89
|
+
/** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert so other peers can attempt a direct connection instead of always falling back to a relay. Omit (or pass none) for a caller with nothing to offer, e.g. a browser client. */
|
|
90
|
+
addresses?: readonly string[]): MeshSession;
|
|
89
91
|
/** A MeshSession built over a connection that already exists (a Transport's own listen() handed it to onConnection), extended with the one thing a dial-side session can't offer: the device-id of the specific peer at the other end. Unlike createMeshSession, which may end up talking to a relay gossiping about many devices at once, an accepted connection is the agent-comms case -- exactly two peers, directly connected -- so "the peer" is well-defined here in a way it structurally isn't for the dial side. */
|
|
90
92
|
export interface AcceptedMeshSession extends MeshSession {
|
|
91
93
|
/** Resolves with the device-id carried by the first peer-advert this connection's remote sends -- the same self-advertisement mechanism createMeshSession's own directory already relies on for every peer, just narrowed to "the one peer this specific connection is with" rather than accumulated into a directory of possibly many. There is no transport-level authentication behind this yet (see wire-mesh#45's own createTlsTransport item): it is only as trustworthy as the remote's own gossip, exactly the same trust level the dial-side directory already has for every entry in it. */
|
|
@@ -95,6 +97,8 @@ export interface AcceptedMeshSessionOptions {
|
|
|
95
97
|
/** A caller-chosen label for this connection, used only for ConnectionState's own address field -- the Connection/Transport ports expose no remote-address concept an accepted connection could report on its own (see wire-mesh#45). Defaults to a fixed placeholder since most callers have nothing more specific to offer; a transport adapter that does know the remote's address should pass it here. */
|
|
96
98
|
label?: string;
|
|
97
99
|
clock?: Readonly<Clock>;
|
|
100
|
+
/** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert. Omit (or pass none) for a caller with nothing to offer. */
|
|
101
|
+
addresses?: readonly string[];
|
|
98
102
|
}
|
|
99
103
|
/** 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. */
|
|
100
104
|
export declare function acceptMeshSession(connection: Readonly<Connection>, identity: Readonly<IdentityPort>, localDomains: readonly string[], options?: Readonly<AcceptedMeshSessionOptions>): Promise<AcceptedMeshSession>;
|
|
@@ -85,7 +85,9 @@ export interface MeshSession {
|
|
|
85
85
|
sendManageRequest: (command: ManageCommand, scope: Readonly<CapabilityScope>, targetDevice?: DeviceId, token?: CapabilityToken) => Promise<ManageOutcome>;
|
|
86
86
|
close: () => Promise<void>;
|
|
87
87
|
}
|
|
88
|
-
export declare function createMeshSession(transport: Readonly<Transport>, identity: Readonly<IdentityPort>, clock?: Readonly<Clock>, reconnect?: ReconnectPolicy | null
|
|
88
|
+
export declare function createMeshSession(transport: Readonly<Transport>, identity: Readonly<IdentityPort>, clock?: Readonly<Clock>, reconnect?: ReconnectPolicy | null,
|
|
89
|
+
/** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert so other peers can attempt a direct connection instead of always falling back to a relay. Omit (or pass none) for a caller with nothing to offer, e.g. a browser client. */
|
|
90
|
+
addresses?: readonly string[]): MeshSession;
|
|
89
91
|
/** A MeshSession built over a connection that already exists (a Transport's own listen() handed it to onConnection), extended with the one thing a dial-side session can't offer: the device-id of the specific peer at the other end. Unlike createMeshSession, which may end up talking to a relay gossiping about many devices at once, an accepted connection is the agent-comms case -- exactly two peers, directly connected -- so "the peer" is well-defined here in a way it structurally isn't for the dial side. */
|
|
90
92
|
export interface AcceptedMeshSession extends MeshSession {
|
|
91
93
|
/** Resolves with the device-id carried by the first peer-advert this connection's remote sends -- the same self-advertisement mechanism createMeshSession's own directory already relies on for every peer, just narrowed to "the one peer this specific connection is with" rather than accumulated into a directory of possibly many. There is no transport-level authentication behind this yet (see wire-mesh#45's own createTlsTransport item): it is only as trustworthy as the remote's own gossip, exactly the same trust level the dial-side directory already has for every entry in it. */
|
|
@@ -95,6 +97,8 @@ export interface AcceptedMeshSessionOptions {
|
|
|
95
97
|
/** A caller-chosen label for this connection, used only for ConnectionState's own address field -- the Connection/Transport ports expose no remote-address concept an accepted connection could report on its own (see wire-mesh#45). Defaults to a fixed placeholder since most callers have nothing more specific to offer; a transport adapter that does know the remote's address should pass it here. */
|
|
96
98
|
label?: string;
|
|
97
99
|
clock?: Readonly<Clock>;
|
|
100
|
+
/** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert. Omit (or pass none) for a caller with nothing to offer. */
|
|
101
|
+
addresses?: readonly string[];
|
|
98
102
|
}
|
|
99
103
|
/** 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. */
|
|
100
104
|
export declare function acceptMeshSession(connection: Readonly<Connection>, identity: Readonly<IdentityPort>, localDomains: readonly string[], options?: Readonly<AcceptedMeshSessionOptions>): Promise<AcceptedMeshSession>;
|
|
@@ -15,7 +15,7 @@ function localHandshake(domains) {
|
|
|
15
15
|
/**
|
|
16
16
|
* 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.
|
|
17
17
|
*/
|
|
18
|
-
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert) {
|
|
18
|
+
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = []) {
|
|
19
19
|
let connection = null;
|
|
20
20
|
let state = { status: "idle" };
|
|
21
21
|
let handshake = { status: "pending" };
|
|
@@ -247,7 +247,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert) {
|
|
|
247
247
|
type: "gossip",
|
|
248
248
|
peers: [{
|
|
249
249
|
device: identity.deviceId,
|
|
250
|
-
addresses: [],
|
|
250
|
+
addresses: [...addresses],
|
|
251
251
|
"snapshot-seconds": Math.floor(clock.now() / MS_PER_SECOND)
|
|
252
252
|
}]
|
|
253
253
|
};
|
|
@@ -409,8 +409,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert) {
|
|
|
409
409
|
}
|
|
410
410
|
};
|
|
411
411
|
}
|
|
412
|
-
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null) {
|
|
413
|
-
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address));
|
|
412
|
+
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = []) {
|
|
413
|
+
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), void 0, addresses);
|
|
414
414
|
return session;
|
|
415
415
|
}
|
|
416
416
|
/** 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. */
|
|
@@ -425,7 +425,7 @@ async function acceptMeshSession(connection, identity, localDomains, options = {
|
|
|
425
425
|
if (peerDeviceIdResolved) return;
|
|
426
426
|
peerDeviceIdResolved = true;
|
|
427
427
|
resolvePeerDeviceId?.(advert.device);
|
|
428
|
-
});
|
|
428
|
+
}, options.addresses);
|
|
429
429
|
await wireUpConnection(connection, options.label ?? "accepted", localDomains);
|
|
430
430
|
return {
|
|
431
431
|
...session,
|