wire-mesh-core 3.0.1 → 3.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/adapters/byte-stream-connection.cjs +79 -0
- package/dist/adapters/byte-stream-connection.d.cts +21 -0
- package/dist/adapters/byte-stream-connection.d.mts +21 -0
- package/dist/adapters/byte-stream-connection.mjs +75 -0
- package/dist/adapters/memory-storage.d.cts +1 -1
- package/dist/adapters/memory-storage.d.mts +1 -1
- package/dist/adapters/node-fs-storage.d.cts +1 -1
- package/dist/adapters/node-fs-storage.d.mts +1 -1
- package/dist/domain/bulk.d.cts +1 -1
- package/dist/domain/bulk.d.mts +1 -1
- package/dist/domain/data-sync.d.cts +1 -1
- package/dist/domain/data-sync.d.mts +1 -1
- package/dist/domain/hub-mailbox.cjs +81 -0
- package/dist/domain/hub-mailbox.d.cts +29 -0
- package/dist/domain/hub-mailbox.d.mts +29 -0
- package/dist/domain/hub-mailbox.mjs +79 -0
- package/dist/domain/mesh-session.d.cts +1 -1
- package/dist/domain/mesh-session.d.mts +1 -1
- package/dist/domain/notice-board.d.cts +1 -1
- package/dist/domain/notice-board.d.mts +1 -1
- package/dist/domain/pinned-address.cjs +39 -0
- package/dist/domain/pinned-address.d.cts +22 -0
- package/dist/domain/pinned-address.d.mts +22 -0
- package/dist/domain/pinned-address.mjs +36 -0
- package/dist/domain/relay-hub.cjs +14 -0
- package/dist/domain/relay-hub.d.cts +10 -0
- package/dist/domain/relay-hub.d.mts +10 -0
- package/dist/domain/relay-hub.mjs +14 -0
- package/dist/ports/storage.d.cts +2 -9
- package/dist/ports/storage.d.mts +2 -9
- package/dist/storage-B403CRyu.d.cts +10 -0
- package/dist/storage-B403CRyu.d.mts +10 -0
- package/package.json +13 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_generated_protocol = require("../generated/protocol.cjs");
|
|
3
|
+
let cbor2 = require("cbor2");
|
|
4
|
+
//#region src/adapters/byte-stream-connection.ts
|
|
5
|
+
const LENGTH_PREFIX_BYTES = 4;
|
|
6
|
+
const BYTES_PER_KIBIBYTE = 1024;
|
|
7
|
+
/** The largest frame body a peer may announce in a length prefix, matching the 100 MiB the `ws` package applies to one message on the same hub, so a WebSocket peer and a byte-stream peer are bounded alike. Without a bound a peer-controlled 4 GiB prefix makes the reader buffer until memory runs out. */
|
|
8
|
+
const MAX_FRAME_BYTES = 100 * (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE);
|
|
9
|
+
/** A frame with an empty body, which carries nothing and is skipped by the reader. A QUIC stream is invisible to the peer until a byte crosses it, so the side that opens a stream writes one of these to make it visible before it has a real frame to send. */
|
|
10
|
+
const STREAM_OPEN_MARKER = new Uint8Array(LENGTH_PREFIX_BYTES);
|
|
11
|
+
/** The bytes that carry one frame on a byte stream: a 4-byte big-endian length, then the frame's CBOR body, the same framing the TCP adapter uses. */
|
|
12
|
+
function encodeLengthPrefixedFrame(frame) {
|
|
13
|
+
const body = (0, cbor2.encode)(frame, cbor2.cdeEncodeOptions);
|
|
14
|
+
const message = new Uint8Array(LENGTH_PREFIX_BYTES + body.length);
|
|
15
|
+
new DataView(message.buffer).setUint32(0, body.length);
|
|
16
|
+
message.set(body, LENGTH_PREFIX_BYTES);
|
|
17
|
+
return message;
|
|
18
|
+
}
|
|
19
|
+
function concat(first, second) {
|
|
20
|
+
const joined = new Uint8Array(first.length + second.length);
|
|
21
|
+
joined.set(first);
|
|
22
|
+
joined.set(second, first.length);
|
|
23
|
+
return joined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Frames arriving on a byte stream, in order, until it ends. An empty body (STREAM_OPEN_MARKER) is skipped. A body that does not decode as CBOR, or a length prefix above MAX_FRAME_BYTES, rejects the iteration (a connection-level failure); a body that decodes but fails frame validation is dropped and the stream continues, since an unrecognised frame from a newer peer is what version negotiation exists to tolerate.
|
|
27
|
+
*/
|
|
28
|
+
async function* readFrames(reader) {
|
|
29
|
+
let buffered = /* @__PURE__ */ new Uint8Array(0);
|
|
30
|
+
try {
|
|
31
|
+
for (;;) {
|
|
32
|
+
const chunk = await reader.read();
|
|
33
|
+
if (chunk.done) return;
|
|
34
|
+
buffered = concat(buffered, chunk.value);
|
|
35
|
+
while (buffered.length >= LENGTH_PREFIX_BYTES) {
|
|
36
|
+
const bodyLength = new DataView(buffered.buffer, buffered.byteOffset).getUint32(0);
|
|
37
|
+
if (bodyLength > 104857600) throw new Error(`frame of ${String(bodyLength)} bytes exceeds the ${String(MAX_FRAME_BYTES)}-byte limit`);
|
|
38
|
+
if (buffered.length < LENGTH_PREFIX_BYTES + bodyLength) break;
|
|
39
|
+
if (bodyLength === 0) {
|
|
40
|
+
buffered = buffered.slice(LENGTH_PREFIX_BYTES);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const body = buffered.slice(LENGTH_PREFIX_BYTES, LENGTH_PREFIX_BYTES + bodyLength);
|
|
44
|
+
buffered = buffered.slice(LENGTH_PREFIX_BYTES + bodyLength);
|
|
45
|
+
const result = require_generated_protocol.frameSchema.safeParse((0, cbor2.decode)(body, cbor2.cdeDecodeOptions));
|
|
46
|
+
if (result.success) yield result.data;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
await reader.cancel(error);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A Connection over an ordered, reliable byte stream, one frame per length-prefixed CBOR body. Pass `opened: true` on the side that opened the stream: it writes a STREAM_OPEN_MARKER first, so the peer sees the stream before the first real frame. Pass `opened: true` on the side that opened the stream, which writes a STREAM_OPEN_MARKER first so the peer sees the stream before the first real frame. `close` ends the writable side and cancels the readable side, so the peer sees the stream finish and this side's `receive()` ends.
|
|
56
|
+
*/
|
|
57
|
+
function connectionFromByteStream(stream, options = { opened: false }) {
|
|
58
|
+
const writer = stream.writable.getWriter();
|
|
59
|
+
const reader = stream.readable.getReader();
|
|
60
|
+
const frames = readFrames(reader);
|
|
61
|
+
const marked = options.opened ? writer.write(STREAM_OPEN_MARKER) : Promise.resolve();
|
|
62
|
+
marked.catch(() => void 0);
|
|
63
|
+
return {
|
|
64
|
+
send: async (frame) => {
|
|
65
|
+
await marked;
|
|
66
|
+
await writer.write(encodeLengthPrefixedFrame(frame));
|
|
67
|
+
},
|
|
68
|
+
receive: () => frames,
|
|
69
|
+
close: async () => {
|
|
70
|
+
await writer.close();
|
|
71
|
+
await reader.cancel();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
exports.MAX_FRAME_BYTES = MAX_FRAME_BYTES;
|
|
77
|
+
exports.STREAM_OPEN_MARKER = STREAM_OPEN_MARKER;
|
|
78
|
+
exports.connectionFromByteStream = connectionFromByteStream;
|
|
79
|
+
exports.encodeLengthPrefixedFrame = encodeLengthPrefixedFrame;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { N as Frame } from "../protocol-DCDae3z4.cjs";
|
|
2
|
+
import { t as Connection } from "../transport-CfW7fsGK.cjs";
|
|
3
|
+
//#region src/adapters/byte-stream-connection.d.ts
|
|
4
|
+
/** The largest frame body a peer may announce in a length prefix, matching the 100 MiB the `ws` package applies to one message on the same hub, so a WebSocket peer and a byte-stream peer are bounded alike. Without a bound a peer-controlled 4 GiB prefix makes the reader buffer until memory runs out. */
|
|
5
|
+
export declare const MAX_FRAME_BYTES: number;
|
|
6
|
+
/** A frame with an empty body, which carries nothing and is skipped by the reader. A QUIC stream is invisible to the peer until a byte crosses it, so the side that opens a stream writes one of these to make it visible before it has a real frame to send. */
|
|
7
|
+
export declare const STREAM_OPEN_MARKER: Uint8Array;
|
|
8
|
+
/** The two halves of an ordered, reliable byte stream, as the Web Streams API models them: a WebTransport bidirectional stream is one, and so is any other stream a runtime exposes in that shape. */
|
|
9
|
+
export interface ByteStream {
|
|
10
|
+
readonly readable: ReadableStream<Uint8Array>;
|
|
11
|
+
readonly writable: WritableStream<Uint8Array>;
|
|
12
|
+
}
|
|
13
|
+
/** The bytes that carry one frame on a byte stream: a 4-byte big-endian length, then the frame's CBOR body, the same framing the TCP adapter uses. */
|
|
14
|
+
export declare function encodeLengthPrefixedFrame(frame: Frame): Uint8Array;
|
|
15
|
+
/**
|
|
16
|
+
* A Connection over an ordered, reliable byte stream, one frame per length-prefixed CBOR body. Pass `opened: true` on the side that opened the stream: it writes a STREAM_OPEN_MARKER first, so the peer sees the stream before the first real frame. Pass `opened: true` on the side that opened the stream, which writes a STREAM_OPEN_MARKER first so the peer sees the stream before the first real frame. `close` ends the writable side and cancels the readable side, so the peer sees the stream finish and this side's `receive()` ends.
|
|
17
|
+
*/
|
|
18
|
+
export declare function connectionFromByteStream(stream: Readonly<ByteStream>, options?: Readonly<{
|
|
19
|
+
opened: boolean;
|
|
20
|
+
}>): Connection;
|
|
21
|
+
//#endregion
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { N as Frame } from "../protocol-DCDae3z4.mjs";
|
|
2
|
+
import { t as Connection } from "../transport-iLBxIkRS.mjs";
|
|
3
|
+
//#region src/adapters/byte-stream-connection.d.ts
|
|
4
|
+
/** The largest frame body a peer may announce in a length prefix, matching the 100 MiB the `ws` package applies to one message on the same hub, so a WebSocket peer and a byte-stream peer are bounded alike. Without a bound a peer-controlled 4 GiB prefix makes the reader buffer until memory runs out. */
|
|
5
|
+
export declare const MAX_FRAME_BYTES: number;
|
|
6
|
+
/** A frame with an empty body, which carries nothing and is skipped by the reader. A QUIC stream is invisible to the peer until a byte crosses it, so the side that opens a stream writes one of these to make it visible before it has a real frame to send. */
|
|
7
|
+
export declare const STREAM_OPEN_MARKER: Uint8Array;
|
|
8
|
+
/** The two halves of an ordered, reliable byte stream, as the Web Streams API models them: a WebTransport bidirectional stream is one, and so is any other stream a runtime exposes in that shape. */
|
|
9
|
+
export interface ByteStream {
|
|
10
|
+
readonly readable: ReadableStream<Uint8Array>;
|
|
11
|
+
readonly writable: WritableStream<Uint8Array>;
|
|
12
|
+
}
|
|
13
|
+
/** The bytes that carry one frame on a byte stream: a 4-byte big-endian length, then the frame's CBOR body, the same framing the TCP adapter uses. */
|
|
14
|
+
export declare function encodeLengthPrefixedFrame(frame: Frame): Uint8Array;
|
|
15
|
+
/**
|
|
16
|
+
* A Connection over an ordered, reliable byte stream, one frame per length-prefixed CBOR body. Pass `opened: true` on the side that opened the stream: it writes a STREAM_OPEN_MARKER first, so the peer sees the stream before the first real frame. Pass `opened: true` on the side that opened the stream, which writes a STREAM_OPEN_MARKER first so the peer sees the stream before the first real frame. `close` ends the writable side and cancels the readable side, so the peer sees the stream finish and this side's `receive()` ends.
|
|
17
|
+
*/
|
|
18
|
+
export declare function connectionFromByteStream(stream: Readonly<ByteStream>, options?: Readonly<{
|
|
19
|
+
opened: boolean;
|
|
20
|
+
}>): Connection;
|
|
21
|
+
//#endregion
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { frameSchema } from "../generated/protocol.mjs";
|
|
2
|
+
import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2";
|
|
3
|
+
//#region src/adapters/byte-stream-connection.ts
|
|
4
|
+
const LENGTH_PREFIX_BYTES = 4;
|
|
5
|
+
const BYTES_PER_KIBIBYTE = 1024;
|
|
6
|
+
/** The largest frame body a peer may announce in a length prefix, matching the 100 MiB the `ws` package applies to one message on the same hub, so a WebSocket peer and a byte-stream peer are bounded alike. Without a bound a peer-controlled 4 GiB prefix makes the reader buffer until memory runs out. */
|
|
7
|
+
const MAX_FRAME_BYTES = 100 * (BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE);
|
|
8
|
+
/** A frame with an empty body, which carries nothing and is skipped by the reader. A QUIC stream is invisible to the peer until a byte crosses it, so the side that opens a stream writes one of these to make it visible before it has a real frame to send. */
|
|
9
|
+
const STREAM_OPEN_MARKER = new Uint8Array(LENGTH_PREFIX_BYTES);
|
|
10
|
+
/** The bytes that carry one frame on a byte stream: a 4-byte big-endian length, then the frame's CBOR body, the same framing the TCP adapter uses. */
|
|
11
|
+
function encodeLengthPrefixedFrame(frame) {
|
|
12
|
+
const body = encode(frame, cdeEncodeOptions);
|
|
13
|
+
const message = new Uint8Array(LENGTH_PREFIX_BYTES + body.length);
|
|
14
|
+
new DataView(message.buffer).setUint32(0, body.length);
|
|
15
|
+
message.set(body, LENGTH_PREFIX_BYTES);
|
|
16
|
+
return message;
|
|
17
|
+
}
|
|
18
|
+
function concat(first, second) {
|
|
19
|
+
const joined = new Uint8Array(first.length + second.length);
|
|
20
|
+
joined.set(first);
|
|
21
|
+
joined.set(second, first.length);
|
|
22
|
+
return joined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Frames arriving on a byte stream, in order, until it ends. An empty body (STREAM_OPEN_MARKER) is skipped. A body that does not decode as CBOR, or a length prefix above MAX_FRAME_BYTES, rejects the iteration (a connection-level failure); a body that decodes but fails frame validation is dropped and the stream continues, since an unrecognised frame from a newer peer is what version negotiation exists to tolerate.
|
|
26
|
+
*/
|
|
27
|
+
async function* readFrames(reader) {
|
|
28
|
+
let buffered = /* @__PURE__ */ new Uint8Array(0);
|
|
29
|
+
try {
|
|
30
|
+
for (;;) {
|
|
31
|
+
const chunk = await reader.read();
|
|
32
|
+
if (chunk.done) return;
|
|
33
|
+
buffered = concat(buffered, chunk.value);
|
|
34
|
+
while (buffered.length >= LENGTH_PREFIX_BYTES) {
|
|
35
|
+
const bodyLength = new DataView(buffered.buffer, buffered.byteOffset).getUint32(0);
|
|
36
|
+
if (bodyLength > 104857600) throw new Error(`frame of ${String(bodyLength)} bytes exceeds the ${String(MAX_FRAME_BYTES)}-byte limit`);
|
|
37
|
+
if (buffered.length < LENGTH_PREFIX_BYTES + bodyLength) break;
|
|
38
|
+
if (bodyLength === 0) {
|
|
39
|
+
buffered = buffered.slice(LENGTH_PREFIX_BYTES);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const body = buffered.slice(LENGTH_PREFIX_BYTES, LENGTH_PREFIX_BYTES + bodyLength);
|
|
43
|
+
buffered = buffered.slice(LENGTH_PREFIX_BYTES + bodyLength);
|
|
44
|
+
const result = frameSchema.safeParse(decode(body, cdeDecodeOptions));
|
|
45
|
+
if (result.success) yield result.data;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
await reader.cancel(error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A Connection over an ordered, reliable byte stream, one frame per length-prefixed CBOR body. Pass `opened: true` on the side that opened the stream: it writes a STREAM_OPEN_MARKER first, so the peer sees the stream before the first real frame. Pass `opened: true` on the side that opened the stream, which writes a STREAM_OPEN_MARKER first so the peer sees the stream before the first real frame. `close` ends the writable side and cancels the readable side, so the peer sees the stream finish and this side's `receive()` ends.
|
|
55
|
+
*/
|
|
56
|
+
function connectionFromByteStream(stream, options = { opened: false }) {
|
|
57
|
+
const writer = stream.writable.getWriter();
|
|
58
|
+
const reader = stream.readable.getReader();
|
|
59
|
+
const frames = readFrames(reader);
|
|
60
|
+
const marked = options.opened ? writer.write(STREAM_OPEN_MARKER) : Promise.resolve();
|
|
61
|
+
marked.catch(() => void 0);
|
|
62
|
+
return {
|
|
63
|
+
send: async (frame) => {
|
|
64
|
+
await marked;
|
|
65
|
+
await writer.write(encodeLengthPrefixedFrame(frame));
|
|
66
|
+
},
|
|
67
|
+
receive: () => frames,
|
|
68
|
+
close: async () => {
|
|
69
|
+
await writer.close();
|
|
70
|
+
await reader.cancel();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { MAX_FRAME_BYTES, STREAM_OPEN_MARKER, connectionFromByteStream, encodeLengthPrefixedFrame };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KeyValueStorage } from "../
|
|
1
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
|
|
2
2
|
//#region src/adapters/memory-storage.d.ts
|
|
3
3
|
/** An in-process KeyValueStorage backed by a Map -- for tests and single-process nodes; a real deployment substitutes a persistent adapter behind the same contract without touching anything that depends on the port. Not declared `async`: every operation is genuinely synchronous under the hood, so the contract's Promise return is satisfied directly via Promise.resolve() rather than an async function with no await in its body. */
|
|
4
4
|
export declare function createMemoryStorage(): KeyValueStorage;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KeyValueStorage } from "../
|
|
1
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
|
|
2
2
|
//#region src/adapters/memory-storage.d.ts
|
|
3
3
|
/** An in-process KeyValueStorage backed by a Map -- for tests and single-process nodes; a real deployment substitutes a persistent adapter behind the same contract without touching anything that depends on the port. Not declared `async`: every operation is genuinely synchronous under the hood, so the contract's Promise return is satisfied directly via Promise.resolve() rather than an async function with no await in its body. */
|
|
4
4
|
export declare function createMemoryStorage(): KeyValueStorage;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KeyValueStorage } from "../
|
|
1
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
|
|
2
2
|
//#region src/adapters/node-fs-storage.d.ts
|
|
3
3
|
export interface NodeFsStorageOptions {
|
|
4
4
|
/** Directory to store entries under. Created on first write if it doesn't already exist. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KeyValueStorage } from "../
|
|
1
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
|
|
2
2
|
//#region src/adapters/node-fs-storage.d.ts
|
|
3
3
|
export interface NodeFsStorageOptions {
|
|
4
4
|
/** Directory to store entries under. Created on first write if it doesn't already exist. */
|
package/dist/domain/bulk.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DCDae3z4.cjs";
|
|
2
|
-
import { KeyValueStorage } from "../ports/storage.cjs";
|
|
3
2
|
import { t as Connection } from "../transport-CfW7fsGK.cjs";
|
|
3
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
|
|
4
4
|
import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
|
|
5
5
|
//#region src/domain/bulk.d.ts
|
|
6
6
|
/** The two capabilities matching the direction each manage-command actually flows (spec/bulk.cddl): the sender proves write authority to open a transfer, the receiver proves read authority to ask for one to continue. */
|
package/dist/domain/bulk.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DCDae3z4.mjs";
|
|
2
|
-
import { KeyValueStorage } from "../ports/storage.mjs";
|
|
3
2
|
import { t as Connection } from "../transport-iLBxIkRS.mjs";
|
|
3
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
|
|
4
4
|
import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
|
|
5
5
|
//#region src/domain/bulk.d.ts
|
|
6
6
|
/** The two capabilities matching the direction each manage-command actually flows (spec/bulk.cddl): the sender proves write authority to open a transfer, the receiver proves read authority to ask for one to continue. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DCDae3z4.cjs";
|
|
2
|
-
import { KeyValueStorage } from "../
|
|
2
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
|
|
3
3
|
import { t as IdentityPort } from "../identity-DVCNuFLG.cjs";
|
|
4
4
|
//#region src/domain/data-sync.d.ts
|
|
5
5
|
export interface DataSyncOptions {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DCDae3z4.mjs";
|
|
2
|
-
import { KeyValueStorage } from "../
|
|
2
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
|
|
3
3
|
import { t as IdentityPort } from "../identity-DcaYgjXT.mjs";
|
|
4
4
|
//#region src/domain/data-sync.d.ts
|
|
5
5
|
export interface DataSyncOptions {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_domain_device_id = require("./device-id.cjs");
|
|
3
|
+
const require_domain_data_sync = require("./data-sync.cjs");
|
|
4
|
+
//#region src/domain/hub-mailbox.ts
|
|
5
|
+
/** The data frames a mailbox answers. */
|
|
6
|
+
function isDataFrame(frame) {
|
|
7
|
+
return frame.type === "data-have" || frame.type === "data-request" || frame.type === "data-entries";
|
|
8
|
+
}
|
|
9
|
+
const LOG_COUNT_KEY = "mailbox/log-count";
|
|
10
|
+
function logBytesKey(peer) {
|
|
11
|
+
return `mailbox/${require_domain_device_id.deviceIdToHex(peer)}/bytes`;
|
|
12
|
+
}
|
|
13
|
+
function encodeCount(value) {
|
|
14
|
+
return new TextEncoder().encode(String(value));
|
|
15
|
+
}
|
|
16
|
+
async function readCount(storage, key) {
|
|
17
|
+
const raw = await storage.get(key);
|
|
18
|
+
return raw === void 0 ? 0 : Number(new TextDecoder().decode(raw));
|
|
19
|
+
}
|
|
20
|
+
function sameDevice(a, b) {
|
|
21
|
+
return require_domain_device_id.deviceIdToHex(a) === require_domain_device_id.deviceIdToHex(b);
|
|
22
|
+
}
|
|
23
|
+
function createMailbox(options) {
|
|
24
|
+
const { storage, limits } = options;
|
|
25
|
+
async function handleHave(sender, frame, refuse) {
|
|
26
|
+
if (!sameDevice(sender, frame.peer)) {
|
|
27
|
+
refuse("not-the-owner");
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (await require_domain_data_sync.headSeqFor(storage, frame.peer) === 0) {
|
|
31
|
+
if (await readCount(storage, LOG_COUNT_KEY) >= limits.maxLogs) {
|
|
32
|
+
refuse("log-limit");
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return require_domain_data_sync.handleDataHave(storage, frame);
|
|
37
|
+
}
|
|
38
|
+
async function handleEntries(sender, frame, refuse) {
|
|
39
|
+
if (!sameDevice(sender, frame.peer)) {
|
|
40
|
+
refuse("not-the-owner");
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (frame.entries.some((entry) => entry.length > limits.maxEntryBytes)) {
|
|
44
|
+
refuse("size-limit");
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const held = await readCount(storage, logBytesKey(frame.peer));
|
|
48
|
+
const added = frame.entries.reduce((sum, entry) => sum + entry.length, 0);
|
|
49
|
+
if (held + added > limits.maxLogBytes) {
|
|
50
|
+
refuse("entry-limit");
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const isNewLog = await require_domain_data_sync.headSeqFor(storage, frame.peer) === 0;
|
|
54
|
+
if (isNewLog && await readCount(storage, LOG_COUNT_KEY) >= limits.maxLogs) {
|
|
55
|
+
refuse("log-limit");
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (!(await require_domain_data_sync.handleDataEntries(storage, frame)).ok) {
|
|
59
|
+
refuse("gap");
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
await storage.set(logBytesKey(frame.peer), encodeCount(held + added));
|
|
63
|
+
if (isNewLog && frame.entries.length > 0) await storage.set(LOG_COUNT_KEY, encodeCount(await readCount(storage, LOG_COUNT_KEY) + 1));
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return { async handle(sender, frame, onRefused) {
|
|
67
|
+
const refuse = (reason) => {
|
|
68
|
+
onRefused?.(reason);
|
|
69
|
+
};
|
|
70
|
+
if (sender === void 0) {
|
|
71
|
+
refuse("unregistered-sender");
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (frame.type === "data-have") return handleHave(sender, frame, refuse);
|
|
75
|
+
if (frame.type === "data-entries") return handleEntries(sender, frame, refuse);
|
|
76
|
+
return require_domain_data_sync.handleDataRequest(storage, frame, limits.readBatch);
|
|
77
|
+
} };
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
exports.createMailbox = createMailbox;
|
|
81
|
+
exports.isDataFrame = isDataFrame;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DCDae3z4.cjs";
|
|
2
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
|
|
3
|
+
//#region src/domain/hub-mailbox.d.ts
|
|
4
|
+
export interface MailboxLimits {
|
|
5
|
+
/** How many devices' logs the mailbox will hold at once. */
|
|
6
|
+
readonly maxLogs: number;
|
|
7
|
+
/** How many bytes of entries one log may hold in total. */
|
|
8
|
+
readonly maxLogBytes: number;
|
|
9
|
+
/** How large one entry may be. */
|
|
10
|
+
readonly maxEntryBytes: number;
|
|
11
|
+
/** How many entries one reply carries at most; a longer log takes more rounds. */
|
|
12
|
+
readonly readBatch: number;
|
|
13
|
+
}
|
|
14
|
+
export interface MailboxOptions {
|
|
15
|
+
readonly storage: KeyValueStorage;
|
|
16
|
+
readonly limits: Readonly<MailboxLimits>;
|
|
17
|
+
}
|
|
18
|
+
/** Called for a data frame the mailbox refuses, so the host can report why; the sender is given no reply, since data-domain.cddl defines no error frame. */
|
|
19
|
+
export type MailboxRefusalReason = "not-the-owner" | "unregistered-sender" | "log-limit" | "size-limit" | "entry-limit" | "gap";
|
|
20
|
+
export interface Mailbox {
|
|
21
|
+
/**
|
|
22
|
+
* Handles one data frame from a connection. `sender` is the device the hub has verified that connection to be, or undefined when it has not gossiped one. Returns the frame to send back, or null for none. `onRefused` is told when the frame was refused and why.
|
|
23
|
+
*/
|
|
24
|
+
handle: (sender: DeviceId | undefined, frame: Readonly<DataHaveFrame | DataRequestFrame | DataEntriesFrame>, onRefused?: (reason: MailboxRefusalReason) => void) => Promise<Frame | null>;
|
|
25
|
+
}
|
|
26
|
+
/** The data frames a mailbox answers. */
|
|
27
|
+
export declare function isDataFrame(frame: Readonly<Frame>): frame is DataHaveFrame | DataRequestFrame | DataEntriesFrame;
|
|
28
|
+
export declare function createMailbox(options: Readonly<MailboxOptions>): Mailbox;
|
|
29
|
+
//#endregion
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DCDae3z4.mjs";
|
|
2
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
|
|
3
|
+
//#region src/domain/hub-mailbox.d.ts
|
|
4
|
+
export interface MailboxLimits {
|
|
5
|
+
/** How many devices' logs the mailbox will hold at once. */
|
|
6
|
+
readonly maxLogs: number;
|
|
7
|
+
/** How many bytes of entries one log may hold in total. */
|
|
8
|
+
readonly maxLogBytes: number;
|
|
9
|
+
/** How large one entry may be. */
|
|
10
|
+
readonly maxEntryBytes: number;
|
|
11
|
+
/** How many entries one reply carries at most; a longer log takes more rounds. */
|
|
12
|
+
readonly readBatch: number;
|
|
13
|
+
}
|
|
14
|
+
export interface MailboxOptions {
|
|
15
|
+
readonly storage: KeyValueStorage;
|
|
16
|
+
readonly limits: Readonly<MailboxLimits>;
|
|
17
|
+
}
|
|
18
|
+
/** Called for a data frame the mailbox refuses, so the host can report why; the sender is given no reply, since data-domain.cddl defines no error frame. */
|
|
19
|
+
export type MailboxRefusalReason = "not-the-owner" | "unregistered-sender" | "log-limit" | "size-limit" | "entry-limit" | "gap";
|
|
20
|
+
export interface Mailbox {
|
|
21
|
+
/**
|
|
22
|
+
* Handles one data frame from a connection. `sender` is the device the hub has verified that connection to be, or undefined when it has not gossiped one. Returns the frame to send back, or null for none. `onRefused` is told when the frame was refused and why.
|
|
23
|
+
*/
|
|
24
|
+
handle: (sender: DeviceId | undefined, frame: Readonly<DataHaveFrame | DataRequestFrame | DataEntriesFrame>, onRefused?: (reason: MailboxRefusalReason) => void) => Promise<Frame | null>;
|
|
25
|
+
}
|
|
26
|
+
/** The data frames a mailbox answers. */
|
|
27
|
+
export declare function isDataFrame(frame: Readonly<Frame>): frame is DataHaveFrame | DataRequestFrame | DataEntriesFrame;
|
|
28
|
+
export declare function createMailbox(options: Readonly<MailboxOptions>): Mailbox;
|
|
29
|
+
//#endregion
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { deviceIdToHex } from "./device-id.mjs";
|
|
2
|
+
import { handleDataEntries, handleDataHave, handleDataRequest, headSeqFor } from "./data-sync.mjs";
|
|
3
|
+
//#region src/domain/hub-mailbox.ts
|
|
4
|
+
/** The data frames a mailbox answers. */
|
|
5
|
+
function isDataFrame(frame) {
|
|
6
|
+
return frame.type === "data-have" || frame.type === "data-request" || frame.type === "data-entries";
|
|
7
|
+
}
|
|
8
|
+
const LOG_COUNT_KEY = "mailbox/log-count";
|
|
9
|
+
function logBytesKey(peer) {
|
|
10
|
+
return `mailbox/${deviceIdToHex(peer)}/bytes`;
|
|
11
|
+
}
|
|
12
|
+
function encodeCount(value) {
|
|
13
|
+
return new TextEncoder().encode(String(value));
|
|
14
|
+
}
|
|
15
|
+
async function readCount(storage, key) {
|
|
16
|
+
const raw = await storage.get(key);
|
|
17
|
+
return raw === void 0 ? 0 : Number(new TextDecoder().decode(raw));
|
|
18
|
+
}
|
|
19
|
+
function sameDevice(a, b) {
|
|
20
|
+
return deviceIdToHex(a) === deviceIdToHex(b);
|
|
21
|
+
}
|
|
22
|
+
function createMailbox(options) {
|
|
23
|
+
const { storage, limits } = options;
|
|
24
|
+
async function handleHave(sender, frame, refuse) {
|
|
25
|
+
if (!sameDevice(sender, frame.peer)) {
|
|
26
|
+
refuse("not-the-owner");
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (await headSeqFor(storage, frame.peer) === 0) {
|
|
30
|
+
if (await readCount(storage, LOG_COUNT_KEY) >= limits.maxLogs) {
|
|
31
|
+
refuse("log-limit");
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return handleDataHave(storage, frame);
|
|
36
|
+
}
|
|
37
|
+
async function handleEntries(sender, frame, refuse) {
|
|
38
|
+
if (!sameDevice(sender, frame.peer)) {
|
|
39
|
+
refuse("not-the-owner");
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (frame.entries.some((entry) => entry.length > limits.maxEntryBytes)) {
|
|
43
|
+
refuse("size-limit");
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const held = await readCount(storage, logBytesKey(frame.peer));
|
|
47
|
+
const added = frame.entries.reduce((sum, entry) => sum + entry.length, 0);
|
|
48
|
+
if (held + added > limits.maxLogBytes) {
|
|
49
|
+
refuse("entry-limit");
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const isNewLog = await headSeqFor(storage, frame.peer) === 0;
|
|
53
|
+
if (isNewLog && await readCount(storage, LOG_COUNT_KEY) >= limits.maxLogs) {
|
|
54
|
+
refuse("log-limit");
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (!(await handleDataEntries(storage, frame)).ok) {
|
|
58
|
+
refuse("gap");
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
await storage.set(logBytesKey(frame.peer), encodeCount(held + added));
|
|
62
|
+
if (isNewLog && frame.entries.length > 0) await storage.set(LOG_COUNT_KEY, encodeCount(await readCount(storage, LOG_COUNT_KEY) + 1));
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return { async handle(sender, frame, onRefused) {
|
|
66
|
+
const refuse = (reason) => {
|
|
67
|
+
onRefused?.(reason);
|
|
68
|
+
};
|
|
69
|
+
if (sender === void 0) {
|
|
70
|
+
refuse("unregistered-sender");
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (frame.type === "data-have") return handleHave(sender, frame, refuse);
|
|
74
|
+
if (frame.type === "data-entries") return handleEntries(sender, frame, refuse);
|
|
75
|
+
return handleDataRequest(storage, frame, limits.readBatch);
|
|
76
|
+
} };
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { createMailbox, isDataFrame };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DCDae3z4.cjs";
|
|
2
|
+
import { r as Transport, t as Connection } from "../transport-CfW7fsGK.cjs";
|
|
2
3
|
import { t as IdentityPort } from "../identity-DVCNuFLG.cjs";
|
|
3
4
|
import { t as Clock } from "../clock-DiSx-WKM.cjs";
|
|
4
|
-
import { r as Transport, t as Connection } from "../transport-CfW7fsGK.cjs";
|
|
5
5
|
//#region src/domain/mesh-session.d.ts
|
|
6
6
|
/** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
|
|
7
7
|
export declare const HANDSHAKE_TIMEOUT_MS = 3000;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DCDae3z4.mjs";
|
|
2
|
+
import { r as Transport, t as Connection } from "../transport-iLBxIkRS.mjs";
|
|
2
3
|
import { t as IdentityPort } from "../identity-DcaYgjXT.mjs";
|
|
3
4
|
import { t as Clock } from "../clock-DiSx-WKM.mjs";
|
|
4
|
-
import { r as Transport, t as Connection } from "../transport-iLBxIkRS.mjs";
|
|
5
5
|
//#region src/domain/mesh-session.d.ts
|
|
6
6
|
/** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
|
|
7
7
|
export declare const HANDSHAKE_TIMEOUT_MS = 3000;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, Ot as RoomNotice, T as DataHaveFrame, kt as RoomNoticeClaims, p as CapabilityToken } from "../protocol-DCDae3z4.cjs";
|
|
2
|
-
import { KeyValueStorage } from "../
|
|
2
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
|
|
3
3
|
import { t as IdentityPort } from "../identity-DVCNuFLG.cjs";
|
|
4
4
|
import { t as Clock } from "../clock-DiSx-WKM.cjs";
|
|
5
5
|
import { a as RevocationCheck } from "../tokens-DqFu7iMX.cjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, Ot as RoomNotice, T as DataHaveFrame, kt as RoomNoticeClaims, p as CapabilityToken } from "../protocol-DCDae3z4.mjs";
|
|
2
|
-
import { KeyValueStorage } from "../
|
|
2
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
|
|
3
3
|
import { t as IdentityPort } from "../identity-DcaYgjXT.mjs";
|
|
4
4
|
import { t as Clock } from "../clock-DiSx-WKM.mjs";
|
|
5
5
|
import { a as RevocationCheck } from "../tokens-CBiSYuzp.mjs";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/domain/pinned-address.ts
|
|
3
|
+
const PIN_FRAGMENT = "#sha256=";
|
|
4
|
+
const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
5
|
+
function hexToBytes(hex) {
|
|
6
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
7
|
+
for (let index = 0; index < bytes.length; index++) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
8
|
+
return bytes;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The address a node advertises for WebTransport: `https://host:port#sha256=<hex>`, the address and the hash of the certificate it serves. A client that pins the hash needs nothing else from the network to trust the node, so the pair travels together in one string, which `wire-candidate.address` already permits.
|
|
12
|
+
*/
|
|
13
|
+
function formatPinnedAddress(hostPort, sha256Hex) {
|
|
14
|
+
return `https://${hostPort}${PIN_FRAGMENT}${sha256Hex}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether an address carries a certificate pin, and so is dialled over WebTransport rather than WebSocket.
|
|
18
|
+
*/
|
|
19
|
+
function isPinnedAddress(address) {
|
|
20
|
+
return address.startsWith("https://") && address.includes(PIN_FRAGMENT);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Splits an address of the form `https://host:port#sha256=<hex>`.
|
|
24
|
+
* @throws Error when the address is not `https://` or carries no valid 64-digit lowercase hex hash.
|
|
25
|
+
*/
|
|
26
|
+
function parsePinnedAddress(address) {
|
|
27
|
+
const url = new URL(address);
|
|
28
|
+
const hex = url.hash.startsWith(PIN_FRAGMENT) ? url.hash.slice(8) : "";
|
|
29
|
+
if (url.protocol !== "https:" || !SHA256_HEX_PATTERN.test(hex)) throw new Error(`expected "https://host:port${PIN_FRAGMENT}<64 hex digits>", got "${address}"`);
|
|
30
|
+
url.hash = "";
|
|
31
|
+
return {
|
|
32
|
+
url: url.toString(),
|
|
33
|
+
sha256: hexToBytes(hex)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
exports.formatPinnedAddress = formatPinnedAddress;
|
|
38
|
+
exports.isPinnedAddress = isPinnedAddress;
|
|
39
|
+
exports.parsePinnedAddress = parsePinnedAddress;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/domain/pinned-address.d.ts
|
|
2
|
+
/** A WebTransport address and the SHA-256 hash of the certificate it is expected to serve. */
|
|
3
|
+
export interface PinnedAddress {
|
|
4
|
+
/** `https://host:port/`, the URL to open. */
|
|
5
|
+
readonly url: string;
|
|
6
|
+
/** The certificate's SHA-256 hash, as bytes. */
|
|
7
|
+
readonly sha256: Uint8Array<ArrayBuffer>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The address a node advertises for WebTransport: `https://host:port#sha256=<hex>`, the address and the hash of the certificate it serves. A client that pins the hash needs nothing else from the network to trust the node, so the pair travels together in one string, which `wire-candidate.address` already permits.
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatPinnedAddress(hostPort: string, sha256Hex: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Whether an address carries a certificate pin, and so is dialled over WebTransport rather than WebSocket.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isPinnedAddress(address: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Splits an address of the form `https://host:port#sha256=<hex>`.
|
|
19
|
+
* @throws Error when the address is not `https://` or carries no valid 64-digit lowercase hex hash.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parsePinnedAddress(address: string): PinnedAddress;
|
|
22
|
+
//#endregion
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/domain/pinned-address.d.ts
|
|
2
|
+
/** A WebTransport address and the SHA-256 hash of the certificate it is expected to serve. */
|
|
3
|
+
export interface PinnedAddress {
|
|
4
|
+
/** `https://host:port/`, the URL to open. */
|
|
5
|
+
readonly url: string;
|
|
6
|
+
/** The certificate's SHA-256 hash, as bytes. */
|
|
7
|
+
readonly sha256: Uint8Array<ArrayBuffer>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The address a node advertises for WebTransport: `https://host:port#sha256=<hex>`, the address and the hash of the certificate it serves. A client that pins the hash needs nothing else from the network to trust the node, so the pair travels together in one string, which `wire-candidate.address` already permits.
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatPinnedAddress(hostPort: string, sha256Hex: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Whether an address carries a certificate pin, and so is dialled over WebTransport rather than WebSocket.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isPinnedAddress(address: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Splits an address of the form `https://host:port#sha256=<hex>`.
|
|
19
|
+
* @throws Error when the address is not `https://` or carries no valid 64-digit lowercase hex hash.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parsePinnedAddress(address: string): PinnedAddress;
|
|
22
|
+
//#endregion
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/domain/pinned-address.ts
|
|
2
|
+
const PIN_FRAGMENT = "#sha256=";
|
|
3
|
+
const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
4
|
+
function hexToBytes(hex) {
|
|
5
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
6
|
+
for (let index = 0; index < bytes.length; index++) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
7
|
+
return bytes;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The address a node advertises for WebTransport: `https://host:port#sha256=<hex>`, the address and the hash of the certificate it serves. A client that pins the hash needs nothing else from the network to trust the node, so the pair travels together in one string, which `wire-candidate.address` already permits.
|
|
11
|
+
*/
|
|
12
|
+
function formatPinnedAddress(hostPort, sha256Hex) {
|
|
13
|
+
return `https://${hostPort}${PIN_FRAGMENT}${sha256Hex}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Whether an address carries a certificate pin, and so is dialled over WebTransport rather than WebSocket.
|
|
17
|
+
*/
|
|
18
|
+
function isPinnedAddress(address) {
|
|
19
|
+
return address.startsWith("https://") && address.includes(PIN_FRAGMENT);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Splits an address of the form `https://host:port#sha256=<hex>`.
|
|
23
|
+
* @throws Error when the address is not `https://` or carries no valid 64-digit lowercase hex hash.
|
|
24
|
+
*/
|
|
25
|
+
function parsePinnedAddress(address) {
|
|
26
|
+
const url = new URL(address);
|
|
27
|
+
const hex = url.hash.startsWith(PIN_FRAGMENT) ? url.hash.slice(8) : "";
|
|
28
|
+
if (url.protocol !== "https:" || !SHA256_HEX_PATTERN.test(hex)) throw new Error(`expected "https://host:port${PIN_FRAGMENT}<64 hex digits>", got "${address}"`);
|
|
29
|
+
url.hash = "";
|
|
30
|
+
return {
|
|
31
|
+
url: url.toString(),
|
|
32
|
+
sha256: hexToBytes(hex)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { formatPinnedAddress, isPinnedAddress, parsePinnedAddress };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_domain_peer_advert = require("./peer-advert.cjs");
|
|
3
3
|
const require_domain_advert_extension_policy = require("./advert-extension-policy.cjs");
|
|
4
|
+
const require_domain_hub_mailbox = require("./hub-mailbox.cjs");
|
|
4
5
|
//#region src/domain/relay-hub.ts
|
|
5
6
|
const HEX_RADIX = 16;
|
|
6
7
|
const HEX_DIGITS_PER_BYTE = 2;
|
|
@@ -192,6 +193,19 @@ function createRelayHub(options) {
|
|
|
192
193
|
});
|
|
193
194
|
return;
|
|
194
195
|
}
|
|
196
|
+
if (options.mailbox !== void 0 && require_domain_hub_mailbox.isDataFrame(frame)) {
|
|
197
|
+
const sender = connectionDevice.get(connection);
|
|
198
|
+
const reply = await options.mailbox.handle(sender, frame, (reason) => {
|
|
199
|
+
options.onMailboxRefused?.({
|
|
200
|
+
device: sender,
|
|
201
|
+
reason
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
if (reply !== null) try {
|
|
205
|
+
await connection.send(reply);
|
|
206
|
+
} catch {}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
195
209
|
if (frame.type === "ping") {
|
|
196
210
|
try {
|
|
197
211
|
await connection.send({ type: "pong" });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { D as DeviceId, N as Frame, et as PeerAdvert } from "../protocol-DCDae3z4.cjs";
|
|
2
2
|
import { t as Connection } from "../transport-CfW7fsGK.cjs";
|
|
3
3
|
import { AdvertExtensionPolicy } from "./advert-extension-policy.cjs";
|
|
4
|
+
import { Mailbox, MailboxRefusalReason } from "./hub-mailbox.cjs";
|
|
4
5
|
import { PeerAdvertVerifier } from "./peer-advert.cjs";
|
|
5
6
|
//#region src/domain/relay-hub.d.ts
|
|
6
7
|
/**
|
|
@@ -36,6 +37,15 @@ export interface RelayHubOptions {
|
|
|
36
37
|
readonly extensionPolicy?: AdvertExtensionPolicy;
|
|
37
38
|
/** Called with each advert refused by extensionPolicy, so the host can report it; the advert's sender is given no reply, since the transport spec defines none. */
|
|
38
39
|
readonly onAdvertRefused?: (refusal: Readonly<AdvertRefusal>) => void;
|
|
40
|
+
/**
|
|
41
|
+
* The announcer role: a mailbox that holds other devices' `core/data` logs while they are offline (hub-mailbox.ts). Absent, data frames sent to this hub are ignored, as they always were.
|
|
42
|
+
*/
|
|
43
|
+
readonly mailbox?: Mailbox;
|
|
44
|
+
/** Called with each data frame the mailbox refused and why, so the host can report it; the sender is given no reply. */
|
|
45
|
+
readonly onMailboxRefused?: (refusal: Readonly<{
|
|
46
|
+
device: DeviceId | undefined;
|
|
47
|
+
reason: MailboxRefusalReason;
|
|
48
|
+
}>) => void;
|
|
39
49
|
/** 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. */
|
|
40
50
|
readonly onConnectionStateChanged?: (connection: Readonly<Connection>) => void;
|
|
41
51
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { D as DeviceId, N as Frame, et as PeerAdvert } from "../protocol-DCDae3z4.mjs";
|
|
2
2
|
import { t as Connection } from "../transport-iLBxIkRS.mjs";
|
|
3
3
|
import { AdvertExtensionPolicy } from "./advert-extension-policy.mjs";
|
|
4
|
+
import { Mailbox, MailboxRefusalReason } from "./hub-mailbox.mjs";
|
|
4
5
|
import { PeerAdvertVerifier } from "./peer-advert.mjs";
|
|
5
6
|
//#region src/domain/relay-hub.d.ts
|
|
6
7
|
/**
|
|
@@ -36,6 +37,15 @@ export interface RelayHubOptions {
|
|
|
36
37
|
readonly extensionPolicy?: AdvertExtensionPolicy;
|
|
37
38
|
/** Called with each advert refused by extensionPolicy, so the host can report it; the advert's sender is given no reply, since the transport spec defines none. */
|
|
38
39
|
readonly onAdvertRefused?: (refusal: Readonly<AdvertRefusal>) => void;
|
|
40
|
+
/**
|
|
41
|
+
* The announcer role: a mailbox that holds other devices' `core/data` logs while they are offline (hub-mailbox.ts). Absent, data frames sent to this hub are ignored, as they always were.
|
|
42
|
+
*/
|
|
43
|
+
readonly mailbox?: Mailbox;
|
|
44
|
+
/** Called with each data frame the mailbox refused and why, so the host can report it; the sender is given no reply. */
|
|
45
|
+
readonly onMailboxRefused?: (refusal: Readonly<{
|
|
46
|
+
device: DeviceId | undefined;
|
|
47
|
+
reason: MailboxRefusalReason;
|
|
48
|
+
}>) => void;
|
|
39
49
|
/** 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. */
|
|
40
50
|
readonly onConnectionStateChanged?: (connection: Readonly<Connection>) => void;
|
|
41
51
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { verifyPeerAdvert } from "./peer-advert.mjs";
|
|
2
2
|
import { checkAdvertExtensions } from "./advert-extension-policy.mjs";
|
|
3
|
+
import { isDataFrame } from "./hub-mailbox.mjs";
|
|
3
4
|
//#region src/domain/relay-hub.ts
|
|
4
5
|
const HEX_RADIX = 16;
|
|
5
6
|
const HEX_DIGITS_PER_BYTE = 2;
|
|
@@ -191,6 +192,19 @@ function createRelayHub(options) {
|
|
|
191
192
|
});
|
|
192
193
|
return;
|
|
193
194
|
}
|
|
195
|
+
if (options.mailbox !== void 0 && isDataFrame(frame)) {
|
|
196
|
+
const sender = connectionDevice.get(connection);
|
|
197
|
+
const reply = await options.mailbox.handle(sender, frame, (reason) => {
|
|
198
|
+
options.onMailboxRefused?.({
|
|
199
|
+
device: sender,
|
|
200
|
+
reason
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
if (reply !== null) try {
|
|
204
|
+
await connection.send(reply);
|
|
205
|
+
} catch {}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
194
208
|
if (frame.type === "ping") {
|
|
195
209
|
try {
|
|
196
210
|
await connection.send({ type: "pong" });
|
package/dist/ports/storage.d.cts
CHANGED
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
get: (key: string) => Promise<Uint8Array<ArrayBuffer> | undefined>;
|
|
4
|
-
set: (key: string, value: Uint8Array<ArrayBuffer>) => Promise<void>;
|
|
5
|
-
delete: (key: string) => Promise<void>;
|
|
6
|
-
/** Keys with the given prefix, in no particular order. */
|
|
7
|
-
keys: (prefix: string) => Promise<string[]>;
|
|
8
|
-
}
|
|
9
|
-
//#endregion
|
|
1
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
|
|
2
|
+
export { KeyValueStorage };
|
package/dist/ports/storage.d.mts
CHANGED
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
get: (key: string) => Promise<Uint8Array<ArrayBuffer> | undefined>;
|
|
4
|
-
set: (key: string, value: Uint8Array<ArrayBuffer>) => Promise<void>;
|
|
5
|
-
delete: (key: string) => Promise<void>;
|
|
6
|
-
/** Keys with the given prefix, in no particular order. */
|
|
7
|
-
keys: (prefix: string) => Promise<string[]>;
|
|
8
|
-
}
|
|
9
|
-
//#endregion
|
|
1
|
+
import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
|
|
2
|
+
export { KeyValueStorage };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/ports/storage.d.ts
|
|
2
|
+
interface KeyValueStorage {
|
|
3
|
+
get: (key: string) => Promise<Uint8Array<ArrayBuffer> | undefined>;
|
|
4
|
+
set: (key: string, value: Uint8Array<ArrayBuffer>) => Promise<void>;
|
|
5
|
+
delete: (key: string) => Promise<void>;
|
|
6
|
+
/** Keys with the given prefix, in no particular order. */
|
|
7
|
+
keys: (prefix: string) => Promise<string[]>;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { KeyValueStorage as t };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/ports/storage.d.ts
|
|
2
|
+
interface KeyValueStorage {
|
|
3
|
+
get: (key: string) => Promise<Uint8Array<ArrayBuffer> | undefined>;
|
|
4
|
+
set: (key: string, value: Uint8Array<ArrayBuffer>) => Promise<void>;
|
|
5
|
+
delete: (key: string) => Promise<void>;
|
|
6
|
+
/** Keys with the given prefix, in no particular order. */
|
|
7
|
+
keys: (prefix: string) => Promise<string[]>;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { KeyValueStorage as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wire-mesh-core",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"cbor2": "2.3.0",
|
|
6
6
|
"cddl.js": "1.0.1",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"vitest": "5.0.0"
|
|
27
27
|
},
|
|
28
28
|
"exports": {
|
|
29
|
+
"./adapters/byte-stream-connection": {
|
|
30
|
+
"import": "./dist/adapters/byte-stream-connection.mjs",
|
|
31
|
+
"require": "./dist/adapters/byte-stream-connection.cjs"
|
|
32
|
+
},
|
|
29
33
|
"./adapters/frame-codec": {
|
|
30
34
|
"import": "./dist/adapters/frame-codec.mjs",
|
|
31
35
|
"require": "./dist/adapters/frame-codec.cjs"
|
|
@@ -118,6 +122,10 @@
|
|
|
118
122
|
"import": "./dist/domain/handshake.mjs",
|
|
119
123
|
"require": "./dist/domain/handshake.cjs"
|
|
120
124
|
},
|
|
125
|
+
"./domain/hub-mailbox": {
|
|
126
|
+
"import": "./dist/domain/hub-mailbox.mjs",
|
|
127
|
+
"require": "./dist/domain/hub-mailbox.cjs"
|
|
128
|
+
},
|
|
121
129
|
"./domain/mesh-session": {
|
|
122
130
|
"import": "./dist/domain/mesh-session.mjs",
|
|
123
131
|
"require": "./dist/domain/mesh-session.cjs"
|
|
@@ -138,6 +146,10 @@
|
|
|
138
146
|
"import": "./dist/domain/ping-round-trips.mjs",
|
|
139
147
|
"require": "./dist/domain/ping-round-trips.cjs"
|
|
140
148
|
},
|
|
149
|
+
"./domain/pinned-address": {
|
|
150
|
+
"import": "./dist/domain/pinned-address.mjs",
|
|
151
|
+
"require": "./dist/domain/pinned-address.cjs"
|
|
152
|
+
},
|
|
141
153
|
"./domain/relay-hub": {
|
|
142
154
|
"import": "./dist/domain/relay-hub.mjs",
|
|
143
155
|
"require": "./dist/domain/relay-hub.cjs"
|