@peers-app/peers-sdk 0.30.6 → 0.31.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.
@@ -2,8 +2,11 @@
2
2
  export type MeshEpisodeReason = "startup" | "resume" | "network-change" | "connection-loss";
3
3
  /** Provenance of a dial; explicit requests and group joins are also distinguished. */
4
4
  export type MeshDialReason = "own-device" | "group-network-info" | "broadcast-answer" | "configured-server" | "lan-scan" | "websocket-upgrade" | "incoming" | "direct-request" | "group-join";
5
- /** Bounded path categories used for lifetime counters; attempts also retain the address. */
6
- export type MeshPath = "websocket" | "webrtc" | "cloud" | "discovery";
5
+ /**
6
+ * Bounded path categories used for lifetime counters; attempts also retain the address.
7
+ * `webtransport` is the PWA's direct LAN session to a desktop (absent from older devices' stats).
8
+ */
9
+ export type MeshPath = "websocket" | "webrtc" | "webtransport" | "cloud" | "discovery";
7
10
  /** Failure categories, independent of free-form transport error messages. */
8
11
  export type MeshFailureReason = "ttl0" | "no-route" | "signaling-timeout" | "socket-error" | "socket-timeout" | "handshake" | "rejected" | "closed" | "superseded" | "unsupported" | "sender-not-known"
9
12
  /** This device can't encrypt to the target yet: no owner-signed row or profile for it here. */
@@ -137,7 +140,10 @@ export interface IMeshStats {
137
140
  recentEpisodes: IMeshEpisode[];
138
141
  percentiles: Record<keyof IMeshDurations, IMeshPercentiles>;
139
142
  counters: {
140
- paths: Record<MeshPath, IMeshPathCounters>;
143
+ /** By path; `webtransport` is missing from devices older than that path. */
144
+ paths: Record<Exclude<MeshPath, "webtransport">, IMeshPathCounters> & {
145
+ webtransport?: IMeshPathCounters;
146
+ };
141
147
  lanScan: {
142
148
  scans: number;
143
149
  probes: number;
@@ -59,8 +59,8 @@ export interface BinaryPeerReceiveLimits {
59
59
  export declare const DEFAULT_BINARY_PEER_RECEIVE_LIMITS: Readonly<BinaryPeerReceiveLimits>;
60
60
  /** Options for wrapping a binary transport in a verified Peers connection. */
61
61
  export interface WrapBinaryPeerOptions {
62
- /** Protocol identifier used for server address (e.g., 'wrtc' or 'ws') */
63
- protocol: "wrtc" | "ws";
62
+ /** Protocol identifier, used for the server address and the transport's frame sizes. */
63
+ protocol: "wrtc" | "ws" | "wt";
64
64
  /** Optional hard limits for untrusted inbound messages. */
65
65
  receiveLimits?: Partial<BinaryPeerReceiveLimits>;
66
66
  }
@@ -70,7 +70,7 @@ export interface WrapBinaryPeerOptions {
70
70
  */
71
71
  export declare function createBinaryPeerSocket(connectionId: string, peer: IBinaryPeer, protocol?: string, receiveLimitOverrides?: Partial<BinaryPeerReceiveLimits>): ISocket;
72
72
  /**
73
- * Wraps a binary peer (WebRTC or WebSocket) into a Connection.
73
+ * Wraps a binary peer (WebRTC, WebTransport or WebSocket) into a Connection.
74
74
  * This is the main implementation that handles all binary protocols.
75
75
  */
76
76
  export declare function wrapBinaryPeer(connectionId: string, peer: IBinaryPeer, localDevice: Device, initiator: boolean, options: WrapBinaryPeerOptions, getTrustLevel?: ITrustLevelFn): Connection;
@@ -57,6 +57,14 @@ const WRTC_CONFIG = {
57
57
  lowWaterMark: 32 * 1024, // 32KB
58
58
  yieldBetweenChunks: true, // CRITICAL for Go sidecar pipeline
59
59
  };
60
+ // WebTransport reaches Node through the same sidecar pipe as WebRTC, so it is paced the same
61
+ // way. The sidecar caps a frame at 1 MB, well above this.
62
+ const WT_CONFIG = {
63
+ maxFramePayload: 64 * 1024,
64
+ highWaterMark: 128 * 1024,
65
+ lowWaterMark: 32 * 1024,
66
+ yieldBetweenChunks: true,
67
+ };
60
68
  const WS_CONFIG = {
61
69
  maxFramePayload: 512 * 1024, // 512KB
62
70
  highWaterMark: 4 * 1024 * 1024, // 4MB
@@ -67,6 +75,8 @@ function getTransportConfig(protocol) {
67
75
  switch (protocol) {
68
76
  case "wrtc":
69
77
  return WRTC_CONFIG;
78
+ case "wt":
79
+ return WT_CONFIG;
70
80
  case "ws":
71
81
  return WS_CONFIG;
72
82
  default:
@@ -252,7 +262,7 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
252
262
  const rawBytesHandlers = {};
253
263
  let isClosed = false;
254
264
  const stats = (0, socket_type_1.createSocketStats)();
255
- const transportType = protocol === "wrtc" ? "wrtc" : protocol === "ws" ? "ws" : "unknown";
265
+ const transportType = protocol === "wrtc" || protocol === "ws" || protocol === "wt" ? protocol : "unknown";
256
266
  const config = getTransportConfig(protocol);
257
267
  const receiveLimits = {
258
268
  ...exports.DEFAULT_BINARY_PEER_RECEIVE_LIMITS,
@@ -836,7 +846,7 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
836
846
  }
837
847
  // ─── Connection Wrappers ─────────────────────────────────────────────
838
848
  /**
839
- * Wraps a binary peer (WebRTC or WebSocket) into a Connection.
849
+ * Wraps a binary peer (WebRTC, WebTransport or WebSocket) into a Connection.
840
850
  * This is the main implementation that handles all binary protocols.
841
851
  */
842
852
  function wrapBinaryPeer(connectionId, peer, localDevice, initiator, options, getTrustLevel) {
@@ -34,8 +34,15 @@ export interface ISocketStats {
34
34
  }
35
35
  /** Creates a new stats object for tracking socket throughput */
36
36
  export declare function createSocketStats(): ISocketStats;
37
- /** Underlying transport family of a socket. */
38
- export type TransportType = "ws" | "wrtc" | "unknown";
37
+ /**
38
+ * Underlying transport family of a socket.
39
+ *
40
+ * - `ws`: Socket.IO over a WebSocket (a desktop's TCP listener).
41
+ * - `wrtc`: a WebRTC data channel.
42
+ * - `wt`: a WebTransport session to a desktop's sidecar (the PWA on the LAN). Not WebRTC, so
43
+ * WebRTC-only policy (upgrades, route checks) leaves it alone.
44
+ */
45
+ export type TransportType = "ws" | "wrtc" | "wt" | "unknown";
39
46
  /** Minimal RPC + raw-byte transport abstraction consumed by `Connection`. */
40
47
  export interface ISocket {
41
48
  id?: string;
@@ -43,6 +50,7 @@ export interface ISocket {
43
50
  /**
44
51
  * Transport type for this socket. Used to optimize chunk sizes for raw byte streaming.
45
52
  * - 'wrtc': WebRTC (64KB max due to SCTP limits)
53
+ * - 'wt': WebTransport through the sidecar (sized like WebRTC's sidecar path)
46
54
  * - 'ws': WebSocket (1MB+ chunks OK)
47
55
  */
48
56
  transportType?: TransportType;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * WebTransport addresses: how a desktop tells the PWA where to reach it directly on the LAN.
3
+ *
4
+ * A desktop's sidecar serves WebTransport (HTTP/3 over QUIC) with a self-signed certificate.
5
+ * Browsers accept that certificate when the page passes its SHA-256 hash
6
+ * (`serverCertificateHashes`), so the address carries the hashes:
7
+ *
8
+ * ```text
9
+ * https://192.168.4.31:3333/peers#sha256=<current>,<next>
10
+ * ```
11
+ *
12
+ * The address goes in the device's owner-signed `reachability.addresses`, beside its WebSocket
13
+ * origins, so the hashes are covered by the same signature and older devices verify it unchanged.
14
+ * The `#sha256=` fragment is what marks an address as WebTransport: WebSocket dialers skip it and
15
+ * WebTransport dialers take nothing else. The server publishes the current week's hash and the
16
+ * next one's, so a phone holding last week's row still matches after the desktop rolls over.
17
+ *
18
+ * To dial, the client picks a connection id and opens {@link webTransportSessionUrl}; both ends
19
+ * then name the connection `wt:<id>` ({@link webTransportConnectionId}), because the Peers
20
+ * handshake binds the connection id into its frame keys.
21
+ */
22
+ /** The path a desktop's WebTransport server accepts sessions on. */
23
+ export declare const WEBTRANSPORT_PATH = "/peers";
24
+ /** Every WebTransport connection id starts with this, on both ends. */
25
+ export declare const WEBTRANSPORT_CONNECTION_PREFIX = "wt:";
26
+ /** The most certificate hashes one address carries (the server publishes two). */
27
+ export declare const MAX_WEBTRANSPORT_CERTIFICATE_HASHES = 4;
28
+ /** A parsed WebTransport address. */
29
+ export interface WebTransportAddress {
30
+ /** The session URL without the client's connection id: `https://<host>:<port>/peers`. */
31
+ url: string;
32
+ /** SHA-256 of each certificate the server may present, as unpadded base64url (43 characters). */
33
+ certificateHashes: string[];
34
+ }
35
+ /**
36
+ * Formats the address a desktop publishes for one of its LAN addresses.
37
+ *
38
+ * @param host An IPv4 address, an IPv6 address (bracketed for you), or a hostname.
39
+ * @param port The server's UDP port.
40
+ * @param certificateHashes The hashes to pin, current window first.
41
+ * @throws When the port or a hash is malformed, or there are no hashes or too many.
42
+ */
43
+ export declare function formatWebTransportAddress(host: string, port: number, certificateHashes: readonly string[]): string;
44
+ /** True when an address carries WebTransport certificate hashes (whether or not it's valid). */
45
+ export declare function isWebTransportAddress(address: string): boolean;
46
+ /**
47
+ * Parses a WebTransport address, or returns `undefined` for anything else, including a
48
+ * malformed one: an `https:` URL on {@link WEBTRANSPORT_PATH} with an explicit port, no
49
+ * credentials or query, and one to {@link MAX_WEBTRANSPORT_CERTIFICATE_HASHES} well-formed hashes.
50
+ */
51
+ export declare function parseWebTransportAddress(address: string): WebTransportAddress | undefined;
52
+ /**
53
+ * The URL a client opens for one session: the address's URL with the client's connection id.
54
+ *
55
+ * @param clientId 16 to 64 letters and digits, such as a `newid()`.
56
+ * @throws When the id doesn't fit that shape (the server would refuse it).
57
+ */
58
+ export declare function webTransportSessionUrl(address: WebTransportAddress, clientId: string): string;
59
+ /** The connection id both ends give a session the client opened with `clientId`. */
60
+ export declare function webTransportConnectionId(clientId: string): string;
61
+ /** Decodes one certificate hash to the 32 bytes `serverCertificateHashes` wants. */
62
+ export declare function decodeWebTransportCertificateHash(hash: string): Uint8Array;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * WebTransport addresses: how a desktop tells the PWA where to reach it directly on the LAN.
4
+ *
5
+ * A desktop's sidecar serves WebTransport (HTTP/3 over QUIC) with a self-signed certificate.
6
+ * Browsers accept that certificate when the page passes its SHA-256 hash
7
+ * (`serverCertificateHashes`), so the address carries the hashes:
8
+ *
9
+ * ```text
10
+ * https://192.168.4.31:3333/peers#sha256=<current>,<next>
11
+ * ```
12
+ *
13
+ * The address goes in the device's owner-signed `reachability.addresses`, beside its WebSocket
14
+ * origins, so the hashes are covered by the same signature and older devices verify it unchanged.
15
+ * The `#sha256=` fragment is what marks an address as WebTransport: WebSocket dialers skip it and
16
+ * WebTransport dialers take nothing else. The server publishes the current week's hash and the
17
+ * next one's, so a phone holding last week's row still matches after the desktop rolls over.
18
+ *
19
+ * To dial, the client picks a connection id and opens {@link webTransportSessionUrl}; both ends
20
+ * then name the connection `wt:<id>` ({@link webTransportConnectionId}), because the Peers
21
+ * handshake binds the connection id into its frame keys.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.MAX_WEBTRANSPORT_CERTIFICATE_HASHES = exports.WEBTRANSPORT_CONNECTION_PREFIX = exports.WEBTRANSPORT_PATH = void 0;
25
+ exports.formatWebTransportAddress = formatWebTransportAddress;
26
+ exports.isWebTransportAddress = isWebTransportAddress;
27
+ exports.parseWebTransportAddress = parseWebTransportAddress;
28
+ exports.webTransportSessionUrl = webTransportSessionUrl;
29
+ exports.webTransportConnectionId = webTransportConnectionId;
30
+ exports.decodeWebTransportCertificateHash = decodeWebTransportCertificateHash;
31
+ const keys_1 = require("../keys");
32
+ /** The path a desktop's WebTransport server accepts sessions on. */
33
+ exports.WEBTRANSPORT_PATH = "/peers";
34
+ /** Every WebTransport connection id starts with this, on both ends. */
35
+ exports.WEBTRANSPORT_CONNECTION_PREFIX = "wt:";
36
+ /** The most certificate hashes one address carries (the server publishes two). */
37
+ exports.MAX_WEBTRANSPORT_CERTIFICATE_HASHES = 4;
38
+ const HASH_FRAGMENT = "sha256=";
39
+ const HASH_PATTERN = /^[A-Za-z0-9_-]{43}$/;
40
+ const CLIENT_ID_PATTERN = /^[A-Za-z0-9]{16,64}$/;
41
+ /**
42
+ * Formats the address a desktop publishes for one of its LAN addresses.
43
+ *
44
+ * @param host An IPv4 address, an IPv6 address (bracketed for you), or a hostname.
45
+ * @param port The server's UDP port.
46
+ * @param certificateHashes The hashes to pin, current window first.
47
+ * @throws When the port or a hash is malformed, or there are no hashes or too many.
48
+ */
49
+ function formatWebTransportAddress(host, port, certificateHashes) {
50
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
51
+ throw new Error(`Invalid WebTransport port ${port}`);
52
+ }
53
+ if (certificateHashes.length === 0 ||
54
+ certificateHashes.length > exports.MAX_WEBTRANSPORT_CERTIFICATE_HASHES ||
55
+ !certificateHashes.every((hash) => HASH_PATTERN.test(hash))) {
56
+ throw new Error("Invalid WebTransport certificate hashes");
57
+ }
58
+ const hostPart = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
59
+ return `https://${hostPart}:${port}${exports.WEBTRANSPORT_PATH}#${HASH_FRAGMENT}${certificateHashes.join(",")}`;
60
+ }
61
+ /** True when an address carries WebTransport certificate hashes (whether or not it's valid). */
62
+ function isWebTransportAddress(address) {
63
+ return address.includes(`#${HASH_FRAGMENT}`);
64
+ }
65
+ /**
66
+ * Parses a WebTransport address, or returns `undefined` for anything else, including a
67
+ * malformed one: an `https:` URL on {@link WEBTRANSPORT_PATH} with an explicit port, no
68
+ * credentials or query, and one to {@link MAX_WEBTRANSPORT_CERTIFICATE_HASHES} well-formed hashes.
69
+ */
70
+ function parseWebTransportAddress(address) {
71
+ if (!isWebTransportAddress(address))
72
+ return undefined;
73
+ let url;
74
+ try {
75
+ url = new URL(address);
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ if (url.protocol !== "https:" ||
81
+ url.pathname !== exports.WEBTRANSPORT_PATH ||
82
+ // URL normalizes an explicit :443 away. It is still a valid configured UDP port.
83
+ (!url.port && !/^https:\/\/[^/?#]+:443\//i.test(address)) ||
84
+ url.username ||
85
+ url.password ||
86
+ url.search ||
87
+ !url.hash.startsWith(`#${HASH_FRAGMENT}`)) {
88
+ return undefined;
89
+ }
90
+ const certificateHashes = url.hash.slice(1 + HASH_FRAGMENT.length).split(",");
91
+ if (certificateHashes.length > exports.MAX_WEBTRANSPORT_CERTIFICATE_HASHES ||
92
+ !certificateHashes.every((hash) => HASH_PATTERN.test(hash))) {
93
+ return undefined;
94
+ }
95
+ return { url: `https://${url.host}${exports.WEBTRANSPORT_PATH}`, certificateHashes };
96
+ }
97
+ /**
98
+ * The URL a client opens for one session: the address's URL with the client's connection id.
99
+ *
100
+ * @param clientId 16 to 64 letters and digits, such as a `newid()`.
101
+ * @throws When the id doesn't fit that shape (the server would refuse it).
102
+ */
103
+ function webTransportSessionUrl(address, clientId) {
104
+ if (!CLIENT_ID_PATTERN.test(clientId)) {
105
+ throw new Error("Invalid WebTransport client id");
106
+ }
107
+ return `${address.url}?c=${clientId}`;
108
+ }
109
+ /** The connection id both ends give a session the client opened with `clientId`. */
110
+ function webTransportConnectionId(clientId) {
111
+ return `${exports.WEBTRANSPORT_CONNECTION_PREFIX}${clientId}`;
112
+ }
113
+ /** Decodes one certificate hash to the 32 bytes `serverCertificateHashes` wants. */
114
+ function decodeWebTransportCertificateHash(hash) {
115
+ if (!HASH_PATTERN.test(hash)) {
116
+ throw new Error("Invalid WebTransport certificate hash");
117
+ }
118
+ const base64 = `${hash.replace(/-/g, "+").replace(/_/g, "/")}=`;
119
+ return (0, keys_1.decodeBase64)(base64);
120
+ }
package/dist/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export * from "./device/get-trust-level-fn";
19
19
  export * from "./device/socket.type";
20
20
  export * from "./device/socket-io-binary-peer";
21
21
  export * from "./device/tx-encoding";
22
+ export * from "./device/webtransport-address";
22
23
  export * from "./device-pairing";
23
24
  export * from "./events";
24
25
  export * from "./hlc";
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ __exportStar(require("./device/get-trust-level-fn"), exports);
42
42
  __exportStar(require("./device/socket.type"), exports);
43
43
  __exportStar(require("./device/socket-io-binary-peer"), exports);
44
44
  __exportStar(require("./device/tx-encoding"), exports);
45
+ __exportStar(require("./device/webtransport-address"), exports);
45
46
  __exportStar(require("./device-pairing"), exports);
46
47
  __exportStar(require("./events"), exports);
47
48
  __exportStar(require("./hlc"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.30.6",
3
+ "version": "0.31.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"