@jsprismarine/nethernet 0.0.0 → 0.20.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +17 -2
  4. package/dist/HttpSignaling.d.ts +60 -0
  5. package/dist/HttpSignaling.d.ts.map +1 -0
  6. package/dist/HttpSignaling.js +177 -0
  7. package/dist/HttpSignaling.js.map +1 -0
  8. package/dist/LanAdvertisement.d.ts +36 -0
  9. package/dist/LanAdvertisement.d.ts.map +1 -0
  10. package/dist/LanAdvertisement.js +70 -0
  11. package/dist/LanAdvertisement.js.map +1 -0
  12. package/dist/NetherNetDefaults.d.ts +3 -0
  13. package/dist/NetherNetDefaults.d.ts.map +1 -0
  14. package/dist/NetherNetDefaults.js +6 -0
  15. package/dist/NetherNetDefaults.js.map +1 -0
  16. package/dist/NetherNetIdentity.d.ts +88 -0
  17. package/dist/NetherNetIdentity.d.ts.map +1 -0
  18. package/dist/NetherNetIdentity.js +164 -0
  19. package/dist/NetherNetIdentity.js.map +1 -0
  20. package/dist/NetherNetStatus.d.ts +27 -0
  21. package/dist/NetherNetStatus.d.ts.map +1 -0
  22. package/dist/NetherNetStatus.js +46 -0
  23. package/dist/NetherNetStatus.js.map +1 -0
  24. package/dist/constrainIcePorts.d.ts +14 -0
  25. package/dist/constrainIcePorts.d.ts.map +1 -0
  26. package/dist/constrainIcePorts.js +35 -0
  27. package/dist/constrainIcePorts.js.map +1 -0
  28. package/dist/es384.d.ts +26 -0
  29. package/dist/es384.d.ts.map +1 -0
  30. package/dist/es384.js +50 -0
  31. package/dist/es384.js.map +1 -0
  32. package/dist/index.d.ts +19 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +12 -0
  35. package/dist/isRecord.d.ts +3 -0
  36. package/dist/isRecord.d.ts.map +1 -0
  37. package/dist/isRecord.js +6 -0
  38. package/dist/isRecord.js.map +1 -0
  39. package/dist/joinByAddress.d.ts +41 -0
  40. package/dist/joinByAddress.d.ts.map +1 -0
  41. package/dist/joinByAddress.js +52 -0
  42. package/dist/joinByAddress.js.map +1 -0
  43. package/dist/sdp.d.ts +26 -0
  44. package/dist/sdp.d.ts.map +1 -0
  45. package/dist/sdp.js +43 -0
  46. package/dist/sdp.js.map +1 -0
  47. package/dist/tracePeerConnection.d.ts +15 -0
  48. package/dist/tracePeerConnection.d.ts.map +1 -0
  49. package/dist/tracePeerConnection.js +44 -0
  50. package/dist/tracePeerConnection.js.map +1 -0
  51. package/dist/udpPorts.d.ts +37 -0
  52. package/dist/udpPorts.d.ts.map +1 -0
  53. package/dist/udpPorts.js +89 -0
  54. package/dist/udpPorts.js.map +1 -0
  55. package/package.json +57 -9
  56. package/index.js +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # @jsprismarine/nethernet
2
+
3
+ ## 0.20.0
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2944](https://github.com/JSPrismarine/JSPrismarine/pull/2944) [`e498e81`](https://github.com/JSPrismarine/JSPrismarine/commit/e498e81b74d4ca6ee2ea27dedc773c6b2fd37ab0) Thanks [@filiphsps](https://github.com/filiphsps)! - New package `@jsprismarine/nethernet` holds the NetherNet pieces a server and a client share: HTTP signaling, the signed `a=identity` on SDP answers, the LAN advertisement codec and the ES384 JSON Web Signatures Bedrock uses. `@jsprismarine/prismarine` uses it for its NetherNet transport and its encryption handshake.
8
+
9
+ - [#2944](https://github.com/JSPrismarine/JSPrismarine/pull/2944) [`2f505cd`](https://github.com/JSPrismarine/JSPrismarine/commit/2f505cdb9140773af493c3f73890473556bed003) Thanks [@filiphsps](https://github.com/filiphsps)! - `@jsprismarine/auth` covers both ends of a Bedrock login. `XboxAuthProvider` signs in to Xbox Live through prismarine-auth, an optional peer dependency loaded only when a provider signs in, and presents the chain and multiplayer token Minecraft services issue for its key. `createAuthProvider` builds a provider by name, and `KeyedAuthProvider` is the base both providers share. A provider's `getMultiplayerToken()` returns its token, and `@jsprismarine/client` signs its NetherNet offer with it, as a signed-in game client does. The server's login verification moved into the package as `verifyLogin`, exported from `@jsprismarine/auth/server`, beside the values both ends agree on in `@jsprismarine/auth/protocol`.
10
+
11
+ Every JWT and JWS is now signed, verified and decoded by `jose` rather than by hand or through fast-jwt, which no package depends on any more. `verifyLogin` fetches the issuer's keys through `jose`'s remote key set, and allows a minute of clock drift on a token's expiry as well as its start.
12
+
13
+ Breaking: `@jsprismarine/prismarine/network/LoginVerifier` is removed; import `verifyLogin`, `LoginRefused`, `MojangKeys` and `useLoginKeys` from `@jsprismarine/auth/server`, and `TOKEN_ISSUER` from `@jsprismarine/auth/protocol`. `MojangKeys` takes a `fetch` implementation instead of a key set loader and a clock. `SessionEncryption.begin` returns a promise.
14
+ - Updated dependencies [[`f030c63`](https://github.com/JSPrismarine/JSPrismarine/commit/f030c63e5c6269288d8dc576bddde160eda3cc74)]:
15
+ - @jsprismarine/binaryutils@0.20.0
16
+ - @jsprismarine/logger@0.20.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JSPrismarine Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,19 @@
1
1
  # @jsprismarine/nethernet
2
2
 
3
- Placeholder release (`0.0.0`, dist-tag `placeholder`) used to reserve the package name
4
- and configure npm trusted publishing. Real releases will be published by CI.
3
+ [![Join the Discord Server](https://img.shields.io/discord/704967868885762108?color=%237289DA&label=Discord)](https://discord.gg/kZZCDyCBbW)
4
+ [![npm](https://img.shields.io/npm/dt/@jsprismarine/nethernet)](https://www.npmjs.com/package/@jsprismarine/nethernet)
5
+
6
+ NetherNet is the WebRTC transport Minecraft: Bedrock Edition uses beside RakNet. This package holds
7
+ what a server and a client both need on top of [`nethernet`](https://www.npmjs.com/package/nethernet),
8
+ which carries the WebRTC connection itself:
9
+
10
+ - `HttpSignaling`, the HTTP signaling a client joining by address uses: `GET /v1/join` for the
11
+ server's status and `POST /v1/join/{networkId}` to trade an SDP offer for an answer.
12
+ - `NetherNetIdentity`, which signs the `a=identity` a client requires on every answer, and the SDP
13
+ helpers it is built on.
14
+ - `encodeLanAdvertisement` and `decodeLanAdvertisement`, the advertisement a server answers LAN
15
+ discovery with, on the fixed UDP port `NETHERNET_LAN_PORT`.
16
+ - `compactEs384`, `detachedEs384` and `verifiesDetachedEs384`, the ES384 JSON Web Signatures Bedrock signs its tokens with, made and checked by [`jose`](https://github.com/panva/jose).
17
+
18
+ It knows nothing about Minecraft packets. The `0xFE` batch id NetherNet leaves out of each message
19
+ is the business of whichever transport carries batches over it.
@@ -0,0 +1,60 @@
1
+ import { SignalStructure } from 'nethernet';
2
+ /** The part of `nethernet`'s `Server` that turns an offer into an answer. */
3
+ export interface NetherNetOfferAnswerer {
4
+ /** Every peer by connection ID, which a new offer's ID must not collide with. */
5
+ readonly connections: ReadonlyMap<bigint, {
6
+ close(reason?: string): void;
7
+ readonly rtcConnection?: unknown;
8
+ }>;
9
+ /** Negotiates a peer for the offer, answering through `respond`. */
10
+ handleOffer(signal: SignalStructure, respond: (signal: SignalStructure) => void): Promise<void>;
11
+ }
12
+ /** Where the signaling server reports what went wrong. `@jsprismarine/logger`'s `Logger` fits. */
13
+ export interface SignalingLogger {
14
+ debug(message: string): void;
15
+ info(message: string): void;
16
+ warn(message: string): void;
17
+ error(error: unknown): void;
18
+ }
19
+ /** What the signaling server needs besides the answerer. */
20
+ export interface HttpSignalingOptions {
21
+ /** Turns an offer into an answer and keeps the peer it negotiates. */
22
+ readonly answerer: NetherNetOfferAnswerer;
23
+ /** The `GET /v1/join` body, read per request. */
24
+ readonly status: () => string;
25
+ /** Adds the server's `a=identity` to an answer; the client refuses one without it. */
26
+ readonly sign: (answer: string) => string | Promise<string>;
27
+ readonly logger: SignalingLogger;
28
+ /** How long an offer may wait for its answer. */
29
+ readonly answerTimeoutMs?: number;
30
+ }
31
+ /**
32
+ * NetherNet's HTTP signaling, which a client joining by address uses: it probes `GET /v1/join`
33
+ * on the game port's TCP side, then posts its complete SDP offer to `POST /v1/join/{networkId}`
34
+ * and reads the complete answer from the response. One request per attempt; trickle ICE is
35
+ * off, so each side's candidates travel inside its description.
36
+ *
37
+ * The offer goes to the same `nethernet` `Server` LAN signaling feeds, so a peer negotiated
38
+ * here reaches the server through `NetherNetTransport` like any other. HTTP carries no
39
+ * connection ID, so this picks one for each offer, unused by any live peer.
40
+ */
41
+ export declare class HttpSignaling {
42
+ private readonly options;
43
+ private readonly server;
44
+ private readonly answerTimeoutMs;
45
+ constructor(options: HttpSignalingOptions);
46
+ /**
47
+ * Listens on TCP.
48
+ * @param port - The game port; NetherNet signals on its TCP side.
49
+ * @param host - The address to bind.
50
+ * @returns The bound port, which differs from `port` when that was 0.
51
+ */
52
+ listen(port: number, host: string): Promise<number>;
53
+ /** Stops listening and drops open requests. */
54
+ close(): void;
55
+ private route;
56
+ private join;
57
+ /** Hands the offer to the answerer under a fresh connection ID and waits for its answer. */
58
+ private negotiate;
59
+ }
60
+ //# sourceMappingURL=HttpSignaling.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HttpSignaling.d.ts","sources":["../src/HttpSignaling.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,eAAe,EAAc,MAAM,WAAW,CAAC;AAaxD,6EAA6E;AAC7E,MAAM,WAAW,sBAAsB;IACnC,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC9G,oEAAoE;IACpE,WAAW,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnG;AAED,kGAAkG;AAClG,MAAM,WAAW,eAAe;IAC5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/B;AAED,4DAA4D;AAC5D,MAAM,WAAW,oBAAoB;IACjC,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,iDAAiD;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;IAC9B,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5D,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,iDAAiD;IACjD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACrC;AAqBD;;;;;;;;;GASG;AACH,qBAAa,aAAa;IAIH,OAAO,CAAC,QAAQ,CAAC,OAAO;IAH3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IAEzC,YAAoC,OAAO,EAAE,oBAAoB,EAOhE;IAED;;;;;OAKG;IACI,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASzD;IAED,+CAA+C;IACxC,KAAK,IAAI,IAAI,CAGnB;YAEa,KAAK;YAyBL,IAAI;IA0BlB,4FAA4F;IAC5F,OAAO,CAAC,SAAS;CAqCpB"}
@@ -0,0 +1,177 @@
1
+ import { withoutIdentity } from "./sdp.js";
2
+ import { describeCandidates, tracePeerConnection } from "./tracePeerConnection.js";
3
+ import { randomBytes } from "node:crypto";
4
+ import http from "node:http";
5
+ import { SignalStructure, SignalType } from "nethernet";
6
+ // src/HttpSignaling.ts
7
+ /** The largest SDP offer accepted. A real offer with every candidate is a few kilobytes. */
8
+ var MAX_OFFER_BYTES = 65536;
9
+ /** How long an offer may wait for its answer; go-nethernet's handler allows the same. */
10
+ var DEFAULT_ANSWER_TIMEOUT_MS = 15e3;
11
+ /** The largest NetherNet network ID, an unsigned 64-bit integer. */
12
+ var MAX_NETWORK_ID = (1n << 64n) - 1n;
13
+ /** Thrown while reading an offer, carrying the status the request is refused with. */
14
+ var RefusedRequest = class extends Error {
15
+ status;
16
+ constructor(status, message) {
17
+ super(message);
18
+ this.status = status;
19
+ }
20
+ };
21
+ /** A random unsigned 64-bit integer. */
22
+ var randomUint64 = () => randomBytes(8).readBigUInt64BE();
23
+ /**
24
+ * NetherNet's HTTP signaling, which a client joining by address uses: it probes `GET /v1/join`
25
+ * on the game port's TCP side, then posts its complete SDP offer to `POST /v1/join/{networkId}`
26
+ * and reads the complete answer from the response. One request per attempt; trickle ICE is
27
+ * off, so each side's candidates travel inside its description.
28
+ *
29
+ * The offer goes to the same `nethernet` `Server` LAN signaling feeds, so a peer negotiated
30
+ * here reaches the server through `NetherNetTransport` like any other. HTTP carries no
31
+ * connection ID, so this picks one for each offer, unused by any live peer.
32
+ */
33
+ var HttpSignaling = class {
34
+ options;
35
+ server;
36
+ answerTimeoutMs;
37
+ constructor(options) {
38
+ this.options = options;
39
+ this.answerTimeoutMs = options.answerTimeoutMs ?? DEFAULT_ANSWER_TIMEOUT_MS;
40
+ this.server = http.createServer({
41
+ requestTimeout: 1e4,
42
+ headersTimeout: 5e3,
43
+ keepAliveTimeout: 0
44
+ }, (request, response) => void this.route(request, response));
45
+ }
46
+ /**
47
+ * Listens on TCP.
48
+ * @param port - The game port; NetherNet signals on its TCP side.
49
+ * @param host - The address to bind.
50
+ * @returns The bound port, which differs from `port` when that was 0.
51
+ */
52
+ listen(port, host) {
53
+ return new Promise((resolve, reject) => {
54
+ this.server.once("error", reject);
55
+ this.server.listen(port, host, () => {
56
+ this.server.off("error", reject);
57
+ const address = this.server.address();
58
+ resolve(typeof address === "object" && address !== null ? address.port : port);
59
+ });
60
+ });
61
+ }
62
+ /** Stops listening and drops open requests. */
63
+ close() {
64
+ this.server.close();
65
+ this.server.closeAllConnections();
66
+ }
67
+ async route(request, response) {
68
+ response.setHeader("Connection", "close");
69
+ const path = (request.url ?? "").split("?")[0];
70
+ try {
71
+ if (request.method === "GET" && path === "/v1/join") {
72
+ this.options.logger.debug(`NetherNet status asked for by ${remoteOf(request)}`);
73
+ response.writeHead(200, { "Content-Type": "application/json" }).end(this.options.status());
74
+ return;
75
+ }
76
+ const networkId = /^\/v1\/join\/([^/]*)$/.exec(path ?? "")?.[1];
77
+ if (request.method === "POST" && networkId !== void 0) {
78
+ await this.join(networkId, request, response);
79
+ return;
80
+ }
81
+ text(response, 404, "Not Found");
82
+ } catch (error) {
83
+ if (error instanceof RefusedRequest) {
84
+ text(response, error.status, error.message);
85
+ return;
86
+ }
87
+ this.options.logger.error(error);
88
+ if (!response.headersSent) text(response, 500, "An error occurred while handling this request");
89
+ }
90
+ }
91
+ async join(networkIdText, request, response) {
92
+ const { logger } = this.options;
93
+ const networkId = parseNetworkId(networkIdText);
94
+ const offer = await readOffer(request);
95
+ const from = `${remoteOf(request)} (network ${networkId})`;
96
+ logger.info(`NetherNet join from ${from}, offering ${describeCandidates(offer).join(", ") || "no candidates"}`);
97
+ const started = Date.now();
98
+ const result = await this.negotiate(networkId, offer);
99
+ if ("answer" in result) {
100
+ const answer = await this.options.sign(result.answer);
101
+ logger.info(`NetherNet join from ${from} answered in ${Date.now() - started} ms as connection ${result.connectionId}, offering ${describeCandidates(answer).join(", ") || "no candidates"}`);
102
+ response.writeHead(200, { "Content-Type": "application/sdp" }).end(answer);
103
+ return;
104
+ }
105
+ if ("error" in result) {
106
+ logger.warn(`NetherNet join from ${from} failed with error ${result.error}`);
107
+ text(response, 500, result.error);
108
+ return;
109
+ }
110
+ logger.warn(`NetherNet join from ${from} timed out waiting for an answer`);
111
+ text(response, 504, "Timed out waiting for answer");
112
+ }
113
+ /** Hands the offer to the answerer under a fresh connection ID and waits for its answer. */
114
+ negotiate(networkId, offer) {
115
+ const { answerer } = this.options;
116
+ let connectionId = randomUint64();
117
+ while (answerer.connections.has(connectionId)) connectionId = randomUint64();
118
+ return new Promise((resolve) => {
119
+ const timeout = setTimeout(() => {
120
+ answerer.connections.get(connectionId)?.close("Timed out waiting for answer");
121
+ resolve({ timedOut: true });
122
+ }, this.answerTimeoutMs);
123
+ const settle = (result) => {
124
+ clearTimeout(timeout);
125
+ resolve(result);
126
+ };
127
+ const respond = (signal) => {
128
+ if (signal.type === SignalType.ConnectResponse) {
129
+ tracePeerConnection(answerer.connections.get(connectionId)?.rtcConnection, (line) => this.options.logger.info(`NetherNet connection ${connectionId}: ${line}`));
130
+ settle({
131
+ answer: signal.data,
132
+ connectionId
133
+ });
134
+ } else if (signal.type === SignalType.ConnectError) settle({ error: signal.data });
135
+ };
136
+ const request = new SignalStructure(SignalType.ConnectRequest, connectionId, withoutIdentity(offer), networkId);
137
+ answerer.handleOffer(request, respond).catch((error) => {
138
+ this.options.logger.error(error);
139
+ settle({ error: "An error occurred while answering" });
140
+ });
141
+ });
142
+ }
143
+ };
144
+ /** Who sent the request, as `address:port`. */
145
+ var remoteOf = (request) => `${request.socket.remoteAddress}:${request.socket.remotePort}`;
146
+ /** Ends the response with a plain-text body, as Bedrock Dedicated Server does for its errors. */
147
+ var text = (response, status, body) => {
148
+ response.writeHead(status, { "Content-Type": "text/plain" }).end(body);
149
+ };
150
+ /**
151
+ * The client's network ID from the path: an unsigned 64-bit integer in decimal.
152
+ * @throws {RefusedRequest} When it is anything else.
153
+ */
154
+ var parseNetworkId = (text) => {
155
+ if (!/^\d{1,20}$/.test(text) || BigInt(text) > MAX_NETWORK_ID) throw new RefusedRequest(400, "Expected /v1/join/{networkId}");
156
+ return BigInt(text);
157
+ };
158
+ /**
159
+ * The request body as text, refused when empty or larger than an offer can be.
160
+ * @throws {RefusedRequest} When the body is empty or too large.
161
+ */
162
+ var readOffer = async (request) => {
163
+ const chunks = [];
164
+ let size = 0;
165
+ for await (const chunk of request) {
166
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
167
+ size += buffer.byteLength;
168
+ if (size > MAX_OFFER_BYTES) throw new RefusedRequest(413, "SDP offer is too large");
169
+ chunks.push(buffer);
170
+ }
171
+ const offer = Buffer.concat(chunks).toString("utf-8");
172
+ if (offer.trim() === "") throw new RefusedRequest(400, "Missing SDP offer in request body");
173
+ return offer;
174
+ };
175
+ export { HttpSignaling };
176
+
177
+ //# sourceMappingURL=HttpSignaling.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HttpSignaling.js","names":[],"sources":["../src/HttpSignaling.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport http from 'node:http';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { AddressInfo } from 'node:net';\n\nimport { SignalStructure, SignalType } from 'nethernet';\nimport { withoutIdentity } from './sdp';\nimport { describeCandidates, tracePeerConnection } from './tracePeerConnection';\n\n/** The largest SDP offer accepted. A real offer with every candidate is a few kilobytes. */\nconst MAX_OFFER_BYTES = 64 * 1024;\n\n/** How long an offer may wait for its answer; go-nethernet's handler allows the same. */\nconst DEFAULT_ANSWER_TIMEOUT_MS = 15_000;\n\n/** The largest NetherNet network ID, an unsigned 64-bit integer. */\nconst MAX_NETWORK_ID = (1n << 64n) - 1n;\n\n/** The part of `nethernet`'s `Server` that turns an offer into an answer. */\nexport interface NetherNetOfferAnswerer {\n /** Every peer by connection ID, which a new offer's ID must not collide with. */\n readonly connections: ReadonlyMap<bigint, { close(reason?: string): void; readonly rtcConnection?: unknown }>;\n /** Negotiates a peer for the offer, answering through `respond`. */\n handleOffer(signal: SignalStructure, respond: (signal: SignalStructure) => void): Promise<void>;\n}\n\n/** Where the signaling server reports what went wrong. `@jsprismarine/logger`'s `Logger` fits. */\nexport interface SignalingLogger {\n debug(message: string): void;\n info(message: string): void;\n warn(message: string): void;\n error(error: unknown): void;\n}\n\n/** What the signaling server needs besides the answerer. */\nexport interface HttpSignalingOptions {\n /** Turns an offer into an answer and keeps the peer it negotiates. */\n readonly answerer: NetherNetOfferAnswerer;\n /** The `GET /v1/join` body, read per request. */\n readonly status: () => string;\n /** Adds the server's `a=identity` to an answer; the client refuses one without it. */\n readonly sign: (answer: string) => string | Promise<string>;\n readonly logger: SignalingLogger;\n /** How long an offer may wait for its answer. */\n readonly answerTimeoutMs?: number;\n}\n\n/** How a negotiation ended. */\ntype Negotiation =\n | { readonly answer: string; readonly connectionId: bigint }\n | { readonly error: string }\n | { readonly timedOut: true };\n\n/** Thrown while reading an offer, carrying the status the request is refused with. */\nclass RefusedRequest extends Error {\n public constructor(\n public readonly status: number,\n message: string\n ) {\n super(message);\n }\n}\n\n/** A random unsigned 64-bit integer. */\nconst randomUint64 = (): bigint => randomBytes(8).readBigUInt64BE();\n\n/**\n * NetherNet's HTTP signaling, which a client joining by address uses: it probes `GET /v1/join`\n * on the game port's TCP side, then posts its complete SDP offer to `POST /v1/join/{networkId}`\n * and reads the complete answer from the response. One request per attempt; trickle ICE is\n * off, so each side's candidates travel inside its description.\n *\n * The offer goes to the same `nethernet` `Server` LAN signaling feeds, so a peer negotiated\n * here reaches the server through `NetherNetTransport` like any other. HTTP carries no\n * connection ID, so this picks one for each offer, unused by any live peer.\n */\nexport class HttpSignaling {\n private readonly server: http.Server;\n private readonly answerTimeoutMs: number;\n\n public constructor(private readonly options: HttpSignalingOptions) {\n this.answerTimeoutMs = options.answerTimeoutMs ?? DEFAULT_ANSWER_TIMEOUT_MS;\n this.server = http.createServer(\n // A client sends one request per attempt, so a connection that lingers is not a client.\n { requestTimeout: 10_000, headersTimeout: 5_000, keepAliveTimeout: 0 },\n (request, response) => void this.route(request, response)\n );\n }\n\n /**\n * Listens on TCP.\n * @param port - The game port; NetherNet signals on its TCP side.\n * @param host - The address to bind.\n * @returns The bound port, which differs from `port` when that was 0.\n */\n public listen(port: number, host: string): Promise<number> {\n return new Promise((resolve, reject) => {\n this.server.once('error', reject);\n this.server.listen(port, host, () => {\n this.server.off('error', reject);\n const address: AddressInfo | string | null = this.server.address();\n resolve(typeof address === 'object' && address !== null ? address.port : port);\n });\n });\n }\n\n /** Stops listening and drops open requests. */\n public close(): void {\n this.server.close();\n this.server.closeAllConnections();\n }\n\n private async route(request: IncomingMessage, response: ServerResponse): Promise<void> {\n response.setHeader('Connection', 'close');\n const path = (request.url ?? '').split('?')[0];\n try {\n if (request.method === 'GET' && path === '/v1/join') {\n this.options.logger.debug(`NetherNet status asked for by ${remoteOf(request)}`);\n response.writeHead(200, { 'Content-Type': 'application/json' }).end(this.options.status());\n return;\n }\n const networkId = /^\\/v1\\/join\\/([^/]*)$/.exec(path ?? '')?.[1];\n if (request.method === 'POST' && networkId !== undefined) {\n await this.join(networkId, request, response);\n return;\n }\n text(response, 404, 'Not Found');\n } catch (error: unknown) {\n if (error instanceof RefusedRequest) {\n text(response, error.status, error.message);\n return;\n }\n this.options.logger.error(error);\n if (!response.headersSent) text(response, 500, 'An error occurred while handling this request');\n }\n }\n\n private async join(networkIdText: string, request: IncomingMessage, response: ServerResponse): Promise<void> {\n const { logger } = this.options;\n const networkId = parseNetworkId(networkIdText);\n const offer = await readOffer(request);\n const from = `${remoteOf(request)} (network ${networkId})`;\n logger.info(`NetherNet join from ${from}, offering ${describeCandidates(offer).join(', ') || 'no candidates'}`);\n\n const started = Date.now();\n const result = await this.negotiate(networkId, offer);\n if ('answer' in result) {\n const answer = await this.options.sign(result.answer);\n logger.info(\n `NetherNet join from ${from} answered in ${Date.now() - started} ms as connection ${result.connectionId}, offering ${describeCandidates(answer).join(', ') || 'no candidates'}`\n );\n response.writeHead(200, { 'Content-Type': 'application/sdp' }).end(answer);\n return;\n }\n if ('error' in result) {\n logger.warn(`NetherNet join from ${from} failed with error ${result.error}`);\n text(response, 500, result.error);\n return;\n }\n logger.warn(`NetherNet join from ${from} timed out waiting for an answer`);\n text(response, 504, 'Timed out waiting for answer');\n }\n\n /** Hands the offer to the answerer under a fresh connection ID and waits for its answer. */\n private negotiate(networkId: bigint, offer: string): Promise<Negotiation> {\n const { answerer } = this.options;\n let connectionId = randomUint64();\n while (answerer.connections.has(connectionId)) connectionId = randomUint64();\n\n return new Promise((resolve) => {\n const timeout = setTimeout(() => {\n answerer.connections.get(connectionId)?.close('Timed out waiting for answer');\n resolve({ timedOut: true });\n }, this.answerTimeoutMs);\n const settle = (result: Negotiation) => {\n clearTimeout(timeout);\n resolve(result);\n };\n\n // The answer already lists every candidate, so later CANDIDATEADD signals are dropped.\n const respond = (signal: SignalStructure) => {\n if (signal.type === SignalType.ConnectResponse) {\n // From here the join is WebRTC's: report each state change until it settles.\n tracePeerConnection(answerer.connections.get(connectionId)?.rtcConnection, (line) =>\n this.options.logger.info(`NetherNet connection ${connectionId}: ${line}`)\n );\n settle({ answer: signal.data, connectionId });\n } else if (signal.type === SignalType.ConnectError) settle({ error: signal.data });\n };\n const request = new SignalStructure(\n SignalType.ConnectRequest,\n connectionId,\n withoutIdentity(offer),\n networkId\n );\n answerer.handleOffer(request, respond).catch((error: unknown) => {\n this.options.logger.error(error);\n settle({ error: 'An error occurred while answering' });\n });\n });\n }\n}\n\n/** Who sent the request, as `address:port`. */\nconst remoteOf = (request: IncomingMessage): string => `${request.socket.remoteAddress}:${request.socket.remotePort}`;\n\n/** Ends the response with a plain-text body, as Bedrock Dedicated Server does for its errors. */\nconst text = (response: ServerResponse, status: number, body: string): void => {\n response.writeHead(status, { 'Content-Type': 'text/plain' }).end(body);\n};\n\n/**\n * The client's network ID from the path: an unsigned 64-bit integer in decimal.\n * @throws {RefusedRequest} When it is anything else.\n */\nconst parseNetworkId = (text: string): bigint => {\n if (!/^\\d{1,20}$/.test(text) || BigInt(text) > MAX_NETWORK_ID) {\n throw new RefusedRequest(400, 'Expected /v1/join/{networkId}');\n }\n return BigInt(text);\n};\n\n/**\n * The request body as text, refused when empty or larger than an offer can be.\n * @throws {RefusedRequest} When the body is empty or too large.\n */\nconst readOffer = async (request: IncomingMessage): Promise<string> => {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of request) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));\n size += buffer.byteLength;\n if (size > MAX_OFFER_BYTES) throw new RefusedRequest(413, 'SDP offer is too large');\n chunks.push(buffer);\n }\n const offer = Buffer.concat(chunks).toString('utf-8');\n if (offer.trim() === '') throw new RefusedRequest(400, 'Missing SDP offer in request body');\n return offer;\n};\n"],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { NetherNetStatus } from './NetherNetStatus';
2
+ /** The advertisement layout Bedrock Dedicated Server 1.26.51 answers LAN discovery with. */
3
+ export declare const LAN_ADVERTISEMENT_VERSION = 7;
4
+ /**
5
+ * What a server tells a client looking for games on the local network, in the answer to a
6
+ * NetherNet discovery request on UDP 7551. The client lists it among its LAN games.
7
+ */
8
+ export interface LanAdvertisement extends NetherNetStatus {
9
+ readonly editorWorld: boolean;
10
+ readonly hardcore: boolean;
11
+ /** Whether a client signed in to Xbox Live may join. */
12
+ readonly acceptsOnlineAuth: boolean;
13
+ /** Whether a client that signs its own login may join, which an online-mode server refuses. */
14
+ readonly acceptsSelfSignedAuth: boolean;
15
+ /** A random token per server start; Bedrock Dedicated Server sends 64 random bits in hex. */
16
+ readonly nonce: string;
17
+ readonly connectionType: number;
18
+ }
19
+ /**
20
+ * The advertisement for a server.
21
+ * @param status - What every NetherNet client is told about the server.
22
+ * @param onlineMode - Whether the server requires Xbox Live sign-in.
23
+ * @param nonce - This server start's token.
24
+ */
25
+ export declare const lanAdvertisementOf: (status: NetherNetStatus, onlineMode: boolean, nonce: string) => LanAdvertisement;
26
+ /**
27
+ * Encodes the advertisement in layout {@link LAN_ADVERTISEMENT_VERSION}: a version byte, then
28
+ * varint-prefixed strings, zigzag varints and booleans in a fixed order.
29
+ */
30
+ export declare const encodeLanAdvertisement: (advertisement: LanAdvertisement) => Buffer;
31
+ /**
32
+ * Decodes an advertisement in layout {@link LAN_ADVERTISEMENT_VERSION}.
33
+ * @throws {Error} When the buffer carries another layout version or ends early.
34
+ */
35
+ export declare const decodeLanAdvertisement: (buffer: Buffer) => LanAdvertisement;
36
+ //# sourceMappingURL=LanAdvertisement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LanAdvertisement.d.ts","sources":["../src/LanAdvertisement.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,4FAA4F;AAC5F,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAK3C;;;GAGG;AACH,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACrD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,wDAAwD;IACxD,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC,+FAA+F;IAC/F,QAAQ,CAAC,qBAAqB,EAAE,OAAO,CAAC;IACxC,6FAA6F;IAC7F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACnC;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,WAAY,eAAe,cAAc,OAAO,SAAS,MAAM,KAAG,gBAQ/F,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,sBAAsB,kBAAmB,gBAAgB,KAAG,MAiBxE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,WAAY,MAAM,KAAG,gBAqBvD,CAAC"}
@@ -0,0 +1,70 @@
1
+ import BinaryStream from "@jsprismarine/binaryutils";
2
+ // src/LanAdvertisement.ts
3
+ /** The advertisement layout Bedrock Dedicated Server 1.26.51 answers LAN discovery with. */
4
+ var LAN_ADVERTISEMENT_VERSION = 7;
5
+ /** The connection type Bedrock Dedicated Server 1.26.51 advertises on the LAN. Its meaning is not documented. */
6
+ var LAN_CONNECTION_TYPE = 4;
7
+ /**
8
+ * The advertisement for a server.
9
+ * @param status - What every NetherNet client is told about the server.
10
+ * @param onlineMode - Whether the server requires Xbox Live sign-in.
11
+ * @param nonce - This server start's token.
12
+ */
13
+ var lanAdvertisementOf = (status, onlineMode, nonce) => ({
14
+ ...status,
15
+ editorWorld: false,
16
+ hardcore: false,
17
+ acceptsOnlineAuth: true,
18
+ acceptsSelfSignedAuth: !onlineMode,
19
+ nonce,
20
+ connectionType: LAN_CONNECTION_TYPE
21
+ });
22
+ /**
23
+ * Encodes the advertisement in layout {@link LAN_ADVERTISEMENT_VERSION}: a version byte, then
24
+ * varint-prefixed strings, zigzag varints and booleans in a fixed order.
25
+ */
26
+ var encodeLanAdvertisement = (advertisement) => {
27
+ const stream = new BinaryStream();
28
+ stream.writeByte(7);
29
+ stream.writeVarString(advertisement.motd);
30
+ stream.writeVarInt(advertisement.protocol);
31
+ stream.writeVarString(advertisement.version);
32
+ stream.writeVarString(advertisement.levelName);
33
+ stream.writeVarInt(advertisement.playerCount);
34
+ stream.writeVarInt(advertisement.maxPlayers);
35
+ stream.writeVarInt(advertisement.gameType);
36
+ stream.writeBoolean(advertisement.editorWorld);
37
+ stream.writeBoolean(advertisement.hardcore);
38
+ stream.writeBoolean(advertisement.acceptsOnlineAuth);
39
+ stream.writeBoolean(advertisement.acceptsSelfSignedAuth);
40
+ stream.writeVarString(advertisement.nonce);
41
+ stream.writeVarInt(advertisement.connectionType);
42
+ return stream.getBuffer();
43
+ };
44
+ /**
45
+ * Decodes an advertisement in layout {@link LAN_ADVERTISEMENT_VERSION}.
46
+ * @throws {Error} When the buffer carries another layout version or ends early.
47
+ */
48
+ var decodeLanAdvertisement = (buffer) => {
49
+ const stream = new BinaryStream(buffer);
50
+ const version = stream.readByte();
51
+ if (version !== 7) throw new Error(`Unsupported LAN advertisement version ${version}`);
52
+ return {
53
+ motd: stream.readVarString(),
54
+ protocol: stream.readVarInt(),
55
+ version: stream.readVarString(),
56
+ levelName: stream.readVarString(),
57
+ playerCount: stream.readVarInt(),
58
+ maxPlayers: stream.readVarInt(),
59
+ gameType: stream.readVarInt(),
60
+ editorWorld: stream.readBoolean(),
61
+ hardcore: stream.readBoolean(),
62
+ acceptsOnlineAuth: stream.readBoolean(),
63
+ acceptsSelfSignedAuth: stream.readBoolean(),
64
+ nonce: stream.readVarString(),
65
+ connectionType: stream.readVarInt()
66
+ };
67
+ };
68
+ export { LAN_ADVERTISEMENT_VERSION, decodeLanAdvertisement, encodeLanAdvertisement, lanAdvertisementOf };
69
+
70
+ //# sourceMappingURL=LanAdvertisement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LanAdvertisement.js","names":[],"sources":["../src/LanAdvertisement.ts"],"sourcesContent":["import BinaryStream from '@jsprismarine/binaryutils';\nimport type { NetherNetStatus } from './NetherNetStatus';\n\n/** The advertisement layout Bedrock Dedicated Server 1.26.51 answers LAN discovery with. */\nexport const LAN_ADVERTISEMENT_VERSION = 7;\n\n/** The connection type Bedrock Dedicated Server 1.26.51 advertises on the LAN. Its meaning is not documented. */\nconst LAN_CONNECTION_TYPE = 4;\n\n/**\n * What a server tells a client looking for games on the local network, in the answer to a\n * NetherNet discovery request on UDP 7551. The client lists it among its LAN games.\n */\nexport interface LanAdvertisement extends NetherNetStatus {\n readonly editorWorld: boolean;\n readonly hardcore: boolean;\n /** Whether a client signed in to Xbox Live may join. */\n readonly acceptsOnlineAuth: boolean;\n /** Whether a client that signs its own login may join, which an online-mode server refuses. */\n readonly acceptsSelfSignedAuth: boolean;\n /** A random token per server start; Bedrock Dedicated Server sends 64 random bits in hex. */\n readonly nonce: string;\n readonly connectionType: number;\n}\n\n/**\n * The advertisement for a server.\n * @param status - What every NetherNet client is told about the server.\n * @param onlineMode - Whether the server requires Xbox Live sign-in.\n * @param nonce - This server start's token.\n */\nexport const lanAdvertisementOf = (status: NetherNetStatus, onlineMode: boolean, nonce: string): LanAdvertisement => ({\n ...status,\n editorWorld: false,\n hardcore: false,\n acceptsOnlineAuth: true,\n acceptsSelfSignedAuth: !onlineMode,\n nonce,\n connectionType: LAN_CONNECTION_TYPE\n});\n\n/**\n * Encodes the advertisement in layout {@link LAN_ADVERTISEMENT_VERSION}: a version byte, then\n * varint-prefixed strings, zigzag varints and booleans in a fixed order.\n */\nexport const encodeLanAdvertisement = (advertisement: LanAdvertisement): Buffer => {\n const stream = new BinaryStream();\n stream.writeByte(LAN_ADVERTISEMENT_VERSION);\n stream.writeVarString(advertisement.motd);\n stream.writeVarInt(advertisement.protocol);\n stream.writeVarString(advertisement.version);\n stream.writeVarString(advertisement.levelName);\n stream.writeVarInt(advertisement.playerCount);\n stream.writeVarInt(advertisement.maxPlayers);\n stream.writeVarInt(advertisement.gameType);\n stream.writeBoolean(advertisement.editorWorld);\n stream.writeBoolean(advertisement.hardcore);\n stream.writeBoolean(advertisement.acceptsOnlineAuth);\n stream.writeBoolean(advertisement.acceptsSelfSignedAuth);\n stream.writeVarString(advertisement.nonce);\n stream.writeVarInt(advertisement.connectionType);\n return stream.getBuffer();\n};\n\n/**\n * Decodes an advertisement in layout {@link LAN_ADVERTISEMENT_VERSION}.\n * @throws {Error} When the buffer carries another layout version or ends early.\n */\nexport const decodeLanAdvertisement = (buffer: Buffer): LanAdvertisement => {\n const stream = new BinaryStream(buffer);\n const version = stream.readByte();\n if (version !== LAN_ADVERTISEMENT_VERSION) {\n throw new Error(`Unsupported LAN advertisement version ${version}`);\n }\n return {\n motd: stream.readVarString(),\n protocol: stream.readVarInt(),\n version: stream.readVarString(),\n levelName: stream.readVarString(),\n playerCount: stream.readVarInt(),\n maxPlayers: stream.readVarInt(),\n gameType: stream.readVarInt(),\n editorWorld: stream.readBoolean(),\n hardcore: stream.readBoolean(),\n acceptsOnlineAuth: stream.readBoolean(),\n acceptsSelfSignedAuth: stream.readBoolean(),\n nonce: stream.readVarString(),\n connectionType: stream.readVarInt()\n };\n};\n"],"mappings":";;;AAIA,IAAa,4BAA4B;;AAGzC,IAAM,sBAAsB;;;;;;;AAwB5B,IAAa,sBAAsB,QAAyB,YAAqB,WAAqC;CAClH,GAAG;CACH,aAAa;CACb,UAAU;CACV,mBAAmB;CACnB,uBAAuB,CAAC;CACxB;CACA,gBAAgB;AACpB;;;;;AAMA,IAAa,0BAA0B,kBAA4C;CAC/E,MAAM,SAAS,IAAI,aAAa;CAChC,OAAO,UAAA,CAAmC;CAC1C,OAAO,eAAe,cAAc,IAAI;CACxC,OAAO,YAAY,cAAc,QAAQ;CACzC,OAAO,eAAe,cAAc,OAAO;CAC3C,OAAO,eAAe,cAAc,SAAS;CAC7C,OAAO,YAAY,cAAc,WAAW;CAC5C,OAAO,YAAY,cAAc,UAAU;CAC3C,OAAO,YAAY,cAAc,QAAQ;CACzC,OAAO,aAAa,cAAc,WAAW;CAC7C,OAAO,aAAa,cAAc,QAAQ;CAC1C,OAAO,aAAa,cAAc,iBAAiB;CACnD,OAAO,aAAa,cAAc,qBAAqB;CACvD,OAAO,eAAe,cAAc,KAAK;CACzC,OAAO,YAAY,cAAc,cAAc;CAC/C,OAAO,OAAO,UAAU;AAC5B;;;;;AAMA,IAAa,0BAA0B,WAAqC;CACxE,MAAM,SAAS,IAAI,aAAa,MAAM;CACtC,MAAM,UAAU,OAAO,SAAS;CAChC,IAAI,YAAA,GACA,MAAM,IAAI,MAAM,yCAAyC,SAAS;CAEtE,OAAO;EACH,MAAM,OAAO,cAAc;EAC3B,UAAU,OAAO,WAAW;EAC5B,SAAS,OAAO,cAAc;EAC9B,WAAW,OAAO,cAAc;EAChC,aAAa,OAAO,WAAW;EAC/B,YAAY,OAAO,WAAW;EAC9B,UAAU,OAAO,WAAW;EAC5B,aAAa,OAAO,YAAY;EAChC,UAAU,OAAO,YAAY;EAC7B,mBAAmB,OAAO,YAAY;EACtC,uBAAuB,OAAO,YAAY;EAC1C,OAAO,OAAO,cAAc;EAC5B,gBAAgB,OAAO,WAAW;CACtC;AACJ"}
@@ -0,0 +1,3 @@
1
+ /** The UDP port NetherNet LAN discovery and signaling use. The protocol fixes it; npm `nethernet` binds it. */
2
+ export declare const NETHERNET_LAN_PORT = 7551;
3
+ //# sourceMappingURL=NetherNetDefaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NetherNetDefaults.d.ts","sources":["../src/NetherNetDefaults.ts"],"names":[],"mappings":"AAAA,+GAA+G;AAC/G,eAAO,MAAM,kBAAkB,OAAO,CAAC"}
@@ -0,0 +1,6 @@
1
+ // src/NetherNetDefaults.ts
2
+ /** The UDP port NetherNet LAN discovery and signaling use. The protocol fixes it; npm `nethernet` binds it. */
3
+ var NETHERNET_LAN_PORT = 7551;
4
+ export { NETHERNET_LAN_PORT };
5
+
6
+ //# sourceMappingURL=NetherNetDefaults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NetherNetDefaults.js","names":[],"sources":["../src/NetherNetDefaults.ts"],"sourcesContent":["/** The UDP port NetherNet LAN discovery and signaling use. The protocol fixes it; npm `nethernet` binds it. */\nexport const NETHERNET_LAN_PORT = 7551;\n"],"mappings":";;AACA,IAAa,qBAAqB"}
@@ -0,0 +1,88 @@
1
+ import type { KeyObject } from 'node:crypto';
2
+ import type { SdpFingerprint } from './sdp';
3
+ /**
4
+ * The fingerprints as canonical JSON: keys in code point order, no whitespace. The client
5
+ * rebuilds these exact bytes from the answer's `a=fingerprint` lines to check the signature.
6
+ * @param fingerprints - The answer's fingerprints, in SDP order.
7
+ */
8
+ export declare const canonicalFingerprints: (fingerprints: readonly SdpFingerprint[]) => string;
9
+ /** How an identity writes its public key into the token's `cpk` claim. */
10
+ export type IdentityKeyFormat =
11
+ /** A JSON Web Key, as servers since 26.40 write it. */
12
+ 'jwk'
13
+ /** Base64 DER, as clients write it, the form the Login packet's keys take. */
14
+ | 'der';
15
+ /** How a {@link NetherNetIdentity} presents itself. */
16
+ export interface NetherNetIdentityOptions {
17
+ /** Shown to players on first use over plain HTTP. Bedrock Dedicated Server says `self`. */
18
+ readonly domain?: string;
19
+ /** How the token carries the public key; a server's is a JWK, a client's base64 DER. */
20
+ readonly keyFormat?: IdentityKeyFormat;
21
+ /** The clock, in milliseconds since the epoch. */
22
+ readonly now?: () => number;
23
+ /**
24
+ * A token an authority issued for this key, used instead of a self-signed one: a signed-in
25
+ * client's multiplayer token from Minecraft's authorization service, whose `cpk` claim names
26
+ * this key. The fingerprints are still signed with the key here.
27
+ */
28
+ readonly token?: string;
29
+ }
30
+ /**
31
+ * A NetherNet identity: a P-384 key whose public half a session description carries, signed, in
32
+ * `a=identity`. A client refuses an answer without one, and Bedrock Dedicated Server refuses an
33
+ * offer without one.
34
+ *
35
+ * A server's key must survive restarts: over plain HTTP the client remembers it the first time
36
+ * and asks the player about any other. {@link NetherNetIdentity.loadOrCreate} keeps it in a file.
37
+ */
38
+ export declare class NetherNetIdentity {
39
+ private readonly privateKey;
40
+ private readonly x5u;
41
+ private readonly cpk;
42
+ private readonly domain;
43
+ private readonly now;
44
+ private readonly issuedToken;
45
+ /**
46
+ * @param privateKey - A P-384 private key.
47
+ * @param options - How the identity presents itself; a server's by default.
48
+ */
49
+ constructor(privateKey: KeyObject, options?: NetherNetIdentityOptions);
50
+ /**
51
+ * A fresh client identity, for one connection.
52
+ * @param domain - The identity provider's domain, as the client names it.
53
+ */
54
+ static client(domain?: string): NetherNetIdentity;
55
+ /**
56
+ * Reads the key from `file`, or makes one and writes it there, readable by its owner only.
57
+ * @param file - Where the PKCS #8 PEM key lives.
58
+ */
59
+ static loadOrCreate(file: string): NetherNetIdentity;
60
+ /**
61
+ * The description with `a=identity` added: a self-signed token carrying the public key as
62
+ * `cpk`, and a detached signature over the description's DTLS fingerprints, which binds the key
63
+ * to the certificate WebRTC will present.
64
+ * @param description - The SDP offer or answer, with its `a=fingerprint` lines.
65
+ * @throws {Error} When the description has no fingerprint to sign.
66
+ */
67
+ sign(description: string): Promise<string>;
68
+ }
69
+ /** What {@link verifySelfSignedIdentity} found in a description it accepted. */
70
+ export interface VerifiedIdentity {
71
+ /** The identity's public key, from the token's `cpk` claim. */
72
+ readonly publicKey: KeyObject;
73
+ /** The domain the identity names, which a player may be shown. */
74
+ readonly domain: string;
75
+ /** The description without `a=identity`, ready for WebRTC. */
76
+ readonly description: string;
77
+ }
78
+ /**
79
+ * Checks a self-signed `a=identity` the way a NetherNet client checks a server's: the token is
80
+ * signed by the key in its own `cpk` claim and not expired, and the detached signature covers this
81
+ * description's fingerprints. Offline clients sign their offers the same way; an online client's
82
+ * token is signed by the authorization service instead, which this does not check.
83
+ * @param description - The SDP description carrying `a=identity`.
84
+ * @param nowMs - The clock, in milliseconds since the epoch.
85
+ * @throws {Error} When the identity is missing, malformed, expired or does not verify.
86
+ */
87
+ export declare const verifySelfSignedIdentity: (description: string, nowMs?: number) => Promise<VerifiedIdentity>;
88
+ //# sourceMappingURL=NetherNetIdentity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NetherNetIdentity.d.ts","sources":["../src/NetherNetIdentity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAK5C;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,iBAAkB,SAAS,cAAc,EAAE,KAAG,MAG1E,CAAC;AAEP,0EAA0E;AAC1E,MAAM,MAAM,iBAAiB;AACzB,uDAAuD;AACrD,KAAK;AACP,8EAA8E;GAC5E,KAAK,CAAC;AAEZ,uDAAuD;AACvD,MAAM,WAAW,wBAAwB;IACrC,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,wFAAwF;IACxF,QAAQ,CAAC,SAAS,CAAC,EAAE,iBAAiB,CAAC;IACvC,kDAAkD;IAClD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAYtB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAX/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IAEjD;;;OAGG;IACH,YACqB,UAAU,EAAE,SAAS,EACtC,OAAO,GAAE,wBAA6B,EAWzC;IAED;;;OAGG;IACH,OAAc,MAAM,CAAC,MAAM,SAAS,GAAG,iBAAiB,CAGvD;IAED;;;OAGG;IACH,OAAc,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAM1D;IAED;;;;;;OAMG;IACU,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBtD;CACJ;AAED,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC7B,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAChC;AAeD;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,gBAAuB,MAAM,qBAAuB,OAAO,CAAC,gBAAgB,CAsChH,CAAC"}