@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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +17 -2
- package/dist/HttpSignaling.d.ts +60 -0
- package/dist/HttpSignaling.d.ts.map +1 -0
- package/dist/HttpSignaling.js +177 -0
- package/dist/HttpSignaling.js.map +1 -0
- package/dist/LanAdvertisement.d.ts +36 -0
- package/dist/LanAdvertisement.d.ts.map +1 -0
- package/dist/LanAdvertisement.js +70 -0
- package/dist/LanAdvertisement.js.map +1 -0
- package/dist/NetherNetDefaults.d.ts +3 -0
- package/dist/NetherNetDefaults.d.ts.map +1 -0
- package/dist/NetherNetDefaults.js +6 -0
- package/dist/NetherNetDefaults.js.map +1 -0
- package/dist/NetherNetIdentity.d.ts +88 -0
- package/dist/NetherNetIdentity.d.ts.map +1 -0
- package/dist/NetherNetIdentity.js +164 -0
- package/dist/NetherNetIdentity.js.map +1 -0
- package/dist/NetherNetStatus.d.ts +27 -0
- package/dist/NetherNetStatus.d.ts.map +1 -0
- package/dist/NetherNetStatus.js +46 -0
- package/dist/NetherNetStatus.js.map +1 -0
- package/dist/constrainIcePorts.d.ts +14 -0
- package/dist/constrainIcePorts.d.ts.map +1 -0
- package/dist/constrainIcePorts.js +35 -0
- package/dist/constrainIcePorts.js.map +1 -0
- package/dist/es384.d.ts +26 -0
- package/dist/es384.d.ts.map +1 -0
- package/dist/es384.js +50 -0
- package/dist/es384.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/isRecord.d.ts +3 -0
- package/dist/isRecord.d.ts.map +1 -0
- package/dist/isRecord.js +6 -0
- package/dist/isRecord.js.map +1 -0
- package/dist/joinByAddress.d.ts +41 -0
- package/dist/joinByAddress.d.ts.map +1 -0
- package/dist/joinByAddress.js +52 -0
- package/dist/joinByAddress.js.map +1 -0
- package/dist/sdp.d.ts +26 -0
- package/dist/sdp.d.ts.map +1 -0
- package/dist/sdp.js +43 -0
- package/dist/sdp.js.map +1 -0
- package/dist/tracePeerConnection.d.ts +15 -0
- package/dist/tracePeerConnection.d.ts.map +1 -0
- package/dist/tracePeerConnection.js +44 -0
- package/dist/tracePeerConnection.js.map +1 -0
- package/dist/udpPorts.d.ts +37 -0
- package/dist/udpPorts.d.ts.map +1 -0
- package/dist/udpPorts.js +89 -0
- package/dist/udpPorts.js.map +1 -0
- package/package.json +57 -9
- package/index.js +0 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { BEDROCK_CURVE, compactEs384, detachedEs384, verifiesDetachedEs384 } from "./es384.js";
|
|
2
|
+
import { isRecord } from "./isRecord.js";
|
|
3
|
+
import { sdpFingerprints, withSessionAttribute, withoutIdentity } from "./sdp.js";
|
|
4
|
+
import { compactVerify, decodeJwt } from "jose";
|
|
5
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync } from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
// src/NetherNetIdentity.ts
|
|
8
|
+
/** How long a signed identity token is valid; go-nethernet and Bedrock Dedicated Server use a minute. */
|
|
9
|
+
var TOKEN_LIFETIME_SECONDS = 60;
|
|
10
|
+
/**
|
|
11
|
+
* The fingerprints as canonical JSON: keys in code point order, no whitespace. The client
|
|
12
|
+
* rebuilds these exact bytes from the answer's `a=fingerprint` lines to check the signature.
|
|
13
|
+
* @param fingerprints - The answer's fingerprints, in SDP order.
|
|
14
|
+
*/
|
|
15
|
+
var canonicalFingerprints = (fingerprints) => JSON.stringify({ fingerprint: fingerprints.map(({ algorithm, digest }) => ({
|
|
16
|
+
algorithm,
|
|
17
|
+
digest
|
|
18
|
+
})) });
|
|
19
|
+
/**
|
|
20
|
+
* A NetherNet identity: a P-384 key whose public half a session description carries, signed, in
|
|
21
|
+
* `a=identity`. A client refuses an answer without one, and Bedrock Dedicated Server refuses an
|
|
22
|
+
* offer without one.
|
|
23
|
+
*
|
|
24
|
+
* A server's key must survive restarts: over plain HTTP the client remembers it the first time
|
|
25
|
+
* and asks the player about any other. {@link NetherNetIdentity.loadOrCreate} keeps it in a file.
|
|
26
|
+
*/
|
|
27
|
+
var NetherNetIdentity = class NetherNetIdentity {
|
|
28
|
+
privateKey;
|
|
29
|
+
x5u;
|
|
30
|
+
cpk;
|
|
31
|
+
domain;
|
|
32
|
+
now;
|
|
33
|
+
issuedToken;
|
|
34
|
+
/**
|
|
35
|
+
* @param privateKey - A P-384 private key.
|
|
36
|
+
* @param options - How the identity presents itself; a server's by default.
|
|
37
|
+
*/
|
|
38
|
+
constructor(privateKey, options = {}) {
|
|
39
|
+
this.privateKey = privateKey;
|
|
40
|
+
const publicKey = createPublicKey(privateKey);
|
|
41
|
+
if (publicKey.asymmetricKeyDetails?.namedCurve !== "secp384r1") throw new Error(`A NetherNet identity key must be ${BEDROCK_CURVE}`);
|
|
42
|
+
this.x5u = publicKey.export({
|
|
43
|
+
type: "spki",
|
|
44
|
+
format: "der"
|
|
45
|
+
}).toString("base64");
|
|
46
|
+
this.cpk = (options.keyFormat ?? "jwk") === "jwk" ? publicKey.export({ format: "jwk" }) : this.x5u;
|
|
47
|
+
this.domain = options.domain ?? "self";
|
|
48
|
+
this.now = options.now ?? Date.now;
|
|
49
|
+
this.issuedToken = options.token;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A fresh client identity, for one connection.
|
|
53
|
+
* @param domain - The identity provider's domain, as the client names it.
|
|
54
|
+
*/
|
|
55
|
+
static client(domain = "self") {
|
|
56
|
+
const { privateKey } = generateKeyPairSync("ec", { namedCurve: BEDROCK_CURVE });
|
|
57
|
+
return new NetherNetIdentity(privateKey, {
|
|
58
|
+
domain,
|
|
59
|
+
keyFormat: "der"
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Reads the key from `file`, or makes one and writes it there, readable by its owner only.
|
|
64
|
+
* @param file - Where the PKCS #8 PEM key lives.
|
|
65
|
+
*/
|
|
66
|
+
static loadOrCreate(file) {
|
|
67
|
+
if (fs.existsSync(file)) return new NetherNetIdentity(createPrivateKey(fs.readFileSync(file)));
|
|
68
|
+
const { privateKey } = generateKeyPairSync("ec", { namedCurve: BEDROCK_CURVE });
|
|
69
|
+
fs.writeFileSync(file, privateKey.export({
|
|
70
|
+
type: "pkcs8",
|
|
71
|
+
format: "pem"
|
|
72
|
+
}), { mode: 384 });
|
|
73
|
+
return new NetherNetIdentity(privateKey);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The description with `a=identity` added: a self-signed token carrying the public key as
|
|
77
|
+
* `cpk`, and a detached signature over the description's DTLS fingerprints, which binds the key
|
|
78
|
+
* to the certificate WebRTC will present.
|
|
79
|
+
* @param description - The SDP offer or answer, with its `a=fingerprint` lines.
|
|
80
|
+
* @throws {Error} When the description has no fingerprint to sign.
|
|
81
|
+
*/
|
|
82
|
+
async sign(description) {
|
|
83
|
+
const fingerprints = sdpFingerprints(description);
|
|
84
|
+
if (fingerprints.length === 0) throw new Error("An SDP description without a fingerprint cannot be signed");
|
|
85
|
+
const issuedAt = Math.floor(this.now() / 1e3);
|
|
86
|
+
const token = this.issuedToken ?? await compactEs384({ x5u: this.x5u }, {
|
|
87
|
+
exp: issuedAt + TOKEN_LIFETIME_SECONDS,
|
|
88
|
+
iat: issuedAt,
|
|
89
|
+
cpk: this.cpk
|
|
90
|
+
}, this.privateKey);
|
|
91
|
+
const identity = {
|
|
92
|
+
assertion: JSON.stringify({
|
|
93
|
+
fingerprints: await detachedEs384(canonicalFingerprints(fingerprints), this.privateKey),
|
|
94
|
+
token
|
|
95
|
+
}),
|
|
96
|
+
idp: {
|
|
97
|
+
domain: this.domain,
|
|
98
|
+
protocol: "default"
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return withSessionAttribute(description, `a=identity:${Buffer.from(JSON.stringify(identity)).toString("base64")}`);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
/** The `cpk` claim as a public key: a JWK, or base64 DER. */
|
|
105
|
+
var claimedKey = (cpk) => {
|
|
106
|
+
if (typeof cpk === "string") return createPublicKey({
|
|
107
|
+
key: Buffer.from(cpk, "base64"),
|
|
108
|
+
format: "der",
|
|
109
|
+
type: "spki"
|
|
110
|
+
});
|
|
111
|
+
if (isRecord(cpk)) {
|
|
112
|
+
const { kty, crv, x, y } = cpk;
|
|
113
|
+
if (typeof kty === "string" && typeof crv === "string" && typeof x === "string" && typeof y === "string") return createPublicKey({
|
|
114
|
+
key: {
|
|
115
|
+
kty,
|
|
116
|
+
crv,
|
|
117
|
+
x,
|
|
118
|
+
y
|
|
119
|
+
},
|
|
120
|
+
format: "jwk"
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
throw new Error("The identity token has no usable cpk claim");
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Checks a self-signed `a=identity` the way a NetherNet client checks a server's: the token is
|
|
127
|
+
* signed by the key in its own `cpk` claim and not expired, and the detached signature covers this
|
|
128
|
+
* description's fingerprints. Offline clients sign their offers the same way; an online client's
|
|
129
|
+
* token is signed by the authorization service instead, which this does not check.
|
|
130
|
+
* @param description - The SDP description carrying `a=identity`.
|
|
131
|
+
* @param nowMs - The clock, in milliseconds since the epoch.
|
|
132
|
+
* @throws {Error} When the identity is missing, malformed, expired or does not verify.
|
|
133
|
+
*/
|
|
134
|
+
var verifySelfSignedIdentity = async (description, nowMs = Date.now()) => {
|
|
135
|
+
const line = description.split(/\r?\n/).find((candidate) => candidate.startsWith("a=identity:"));
|
|
136
|
+
if (line === void 0) throw new Error("The description carries no a=identity");
|
|
137
|
+
const identity = JSON.parse(Buffer.from(line.slice(11), "base64").toString());
|
|
138
|
+
if (!isRecord(identity) || typeof identity.assertion !== "string" || !isRecord(identity.idp)) throw new Error("The a=identity has no assertion");
|
|
139
|
+
const assertion = JSON.parse(identity.assertion);
|
|
140
|
+
if (!isRecord(assertion) || typeof assertion.token !== "string" || typeof assertion.fingerprints !== "string") throw new Error("The identity assertion is malformed");
|
|
141
|
+
let claims;
|
|
142
|
+
try {
|
|
143
|
+
claims = decodeJwt(assertion.token);
|
|
144
|
+
} catch {
|
|
145
|
+
throw new Error("The identity token has no claims");
|
|
146
|
+
}
|
|
147
|
+
const publicKey = claimedKey(claims.cpk);
|
|
148
|
+
try {
|
|
149
|
+
await compactVerify(assertion.token, publicKey, { algorithms: ["ES384"] });
|
|
150
|
+
} catch {
|
|
151
|
+
throw new Error("The identity token is not signed by its own key");
|
|
152
|
+
}
|
|
153
|
+
if (typeof claims.exp === "number" && claims.exp < nowMs / 1e3) throw new Error("The identity token has expired");
|
|
154
|
+
const fingerprints = canonicalFingerprints(sdpFingerprints(description));
|
|
155
|
+
if (!await verifiesDetachedEs384(assertion.fingerprints, fingerprints, publicKey)) throw new Error("The identity does not sign this description's fingerprints");
|
|
156
|
+
return {
|
|
157
|
+
publicKey,
|
|
158
|
+
domain: typeof identity.idp.domain === "string" ? identity.idp.domain : "",
|
|
159
|
+
description: withoutIdentity(description)
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
export { NetherNetIdentity, canonicalFingerprints, verifySelfSignedIdentity };
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=NetherNetIdentity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NetherNetIdentity.js","names":[],"sources":["../src/NetherNetIdentity.ts"],"sourcesContent":["import fs from 'node:fs';\nimport { createPrivateKey, createPublicKey, generateKeyPairSync } from 'node:crypto';\nimport type { KeyObject } from 'node:crypto';\n\nimport { compactVerify, decodeJwt } from 'jose';\nimport { BEDROCK_CURVE, compactEs384, detachedEs384, verifiesDetachedEs384 } from './es384';\nimport { isRecord } from './isRecord';\nimport { sdpFingerprints, withoutIdentity, withSessionAttribute } from './sdp';\nimport type { SdpFingerprint } from './sdp';\n\n/** How long a signed identity token is valid; go-nethernet and Bedrock Dedicated Server use a minute. */\nconst TOKEN_LIFETIME_SECONDS = 60;\n\n/**\n * The fingerprints as canonical JSON: keys in code point order, no whitespace. The client\n * rebuilds these exact bytes from the answer's `a=fingerprint` lines to check the signature.\n * @param fingerprints - The answer's fingerprints, in SDP order.\n */\nexport const canonicalFingerprints = (fingerprints: readonly SdpFingerprint[]): string =>\n JSON.stringify({\n fingerprint: fingerprints.map(({ algorithm, digest }) => ({ algorithm, digest }))\n });\n\n/** How an identity writes its public key into the token's `cpk` claim. */\nexport type IdentityKeyFormat =\n /** A JSON Web Key, as servers since 26.40 write it. */\n | 'jwk'\n /** Base64 DER, as clients write it, the form the Login packet's keys take. */\n | 'der';\n\n/** How a {@link NetherNetIdentity} presents itself. */\nexport interface NetherNetIdentityOptions {\n /** Shown to players on first use over plain HTTP. Bedrock Dedicated Server says `self`. */\n readonly domain?: string;\n /** How the token carries the public key; a server's is a JWK, a client's base64 DER. */\n readonly keyFormat?: IdentityKeyFormat;\n /** The clock, in milliseconds since the epoch. */\n readonly now?: () => number;\n /**\n * A token an authority issued for this key, used instead of a self-signed one: a signed-in\n * client's multiplayer token from Minecraft's authorization service, whose `cpk` claim names\n * this key. The fingerprints are still signed with the key here.\n */\n readonly token?: string;\n}\n\n/**\n * A NetherNet identity: a P-384 key whose public half a session description carries, signed, in\n * `a=identity`. A client refuses an answer without one, and Bedrock Dedicated Server refuses an\n * offer without one.\n *\n * A server's key must survive restarts: over plain HTTP the client remembers it the first time\n * and asks the player about any other. {@link NetherNetIdentity.loadOrCreate} keeps it in a file.\n */\nexport class NetherNetIdentity {\n private readonly x5u: string;\n private readonly cpk: string | object;\n private readonly domain: string;\n private readonly now: () => number;\n private readonly issuedToken: string | undefined;\n\n /**\n * @param privateKey - A P-384 private key.\n * @param options - How the identity presents itself; a server's by default.\n */\n public constructor(\n private readonly privateKey: KeyObject,\n options: NetherNetIdentityOptions = {}\n ) {\n const publicKey = createPublicKey(privateKey);\n if (publicKey.asymmetricKeyDetails?.namedCurve !== BEDROCK_CURVE) {\n throw new Error(`A NetherNet identity key must be ${BEDROCK_CURVE}`);\n }\n this.x5u = publicKey.export({ type: 'spki', format: 'der' }).toString('base64');\n this.cpk = (options.keyFormat ?? 'jwk') === 'jwk' ? publicKey.export({ format: 'jwk' }) : this.x5u;\n this.domain = options.domain ?? 'self';\n this.now = options.now ?? Date.now;\n this.issuedToken = options.token;\n }\n\n /**\n * A fresh client identity, for one connection.\n * @param domain - The identity provider's domain, as the client names it.\n */\n public static client(domain = 'self'): NetherNetIdentity {\n const { privateKey } = generateKeyPairSync('ec', { namedCurve: BEDROCK_CURVE });\n return new NetherNetIdentity(privateKey, { domain, keyFormat: 'der' });\n }\n\n /**\n * Reads the key from `file`, or makes one and writes it there, readable by its owner only.\n * @param file - Where the PKCS #8 PEM key lives.\n */\n public static loadOrCreate(file: string): NetherNetIdentity {\n if (fs.existsSync(file)) return new NetherNetIdentity(createPrivateKey(fs.readFileSync(file)));\n\n const { privateKey } = generateKeyPairSync('ec', { namedCurve: BEDROCK_CURVE });\n fs.writeFileSync(file, privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 });\n return new NetherNetIdentity(privateKey);\n }\n\n /**\n * The description with `a=identity` added: a self-signed token carrying the public key as\n * `cpk`, and a detached signature over the description's DTLS fingerprints, which binds the key\n * to the certificate WebRTC will present.\n * @param description - The SDP offer or answer, with its `a=fingerprint` lines.\n * @throws {Error} When the description has no fingerprint to sign.\n */\n public async sign(description: string): Promise<string> {\n const fingerprints = sdpFingerprints(description);\n if (fingerprints.length === 0) throw new Error('An SDP description without a fingerprint cannot be signed');\n\n const issuedAt = Math.floor(this.now() / 1000);\n const token =\n this.issuedToken ??\n (await compactEs384(\n { x5u: this.x5u },\n { exp: issuedAt + TOKEN_LIFETIME_SECONDS, iat: issuedAt, cpk: this.cpk },\n this.privateKey\n ));\n const identity = {\n // The assertion is JSON inside a JSON string, as NetherNet nests it.\n assertion: JSON.stringify({\n fingerprints: await detachedEs384(canonicalFingerprints(fingerprints), this.privateKey),\n token\n }),\n idp: { domain: this.domain, protocol: 'default' }\n };\n return withSessionAttribute(\n description,\n `a=identity:${Buffer.from(JSON.stringify(identity)).toString('base64')}`\n );\n }\n}\n\n/** What {@link verifySelfSignedIdentity} found in a description it accepted. */\nexport interface VerifiedIdentity {\n /** The identity's public key, from the token's `cpk` claim. */\n readonly publicKey: KeyObject;\n /** The domain the identity names, which a player may be shown. */\n readonly domain: string;\n /** The description without `a=identity`, ready for WebRTC. */\n readonly description: string;\n}\n\n/** The `cpk` claim as a public key: a JWK, or base64 DER. */\nconst claimedKey = (cpk: unknown): KeyObject => {\n if (typeof cpk === 'string')\n return createPublicKey({ key: Buffer.from(cpk, 'base64'), format: 'der', type: 'spki' });\n if (isRecord(cpk)) {\n const { kty, crv, x, y } = cpk;\n if (typeof kty === 'string' && typeof crv === 'string' && typeof x === 'string' && typeof y === 'string') {\n return createPublicKey({ key: { kty, crv, x, y }, format: 'jwk' });\n }\n }\n throw new Error('The identity token has no usable cpk claim');\n};\n\n/**\n * Checks a self-signed `a=identity` the way a NetherNet client checks a server's: the token is\n * signed by the key in its own `cpk` claim and not expired, and the detached signature covers this\n * description's fingerprints. Offline clients sign their offers the same way; an online client's\n * token is signed by the authorization service instead, which this does not check.\n * @param description - The SDP description carrying `a=identity`.\n * @param nowMs - The clock, in milliseconds since the epoch.\n * @throws {Error} When the identity is missing, malformed, expired or does not verify.\n */\nexport const verifySelfSignedIdentity = async (description: string, nowMs = Date.now()): Promise<VerifiedIdentity> => {\n const line = description.split(/\\r?\\n/).find((candidate) => candidate.startsWith('a=identity:'));\n if (line === undefined) throw new Error('The description carries no a=identity');\n\n const identity: unknown = JSON.parse(Buffer.from(line.slice('a=identity:'.length), 'base64').toString());\n if (!isRecord(identity) || typeof identity.assertion !== 'string' || !isRecord(identity.idp)) {\n throw new Error('The a=identity has no assertion');\n }\n const assertion: unknown = JSON.parse(identity.assertion);\n if (!isRecord(assertion) || typeof assertion.token !== 'string' || typeof assertion.fingerprints !== 'string') {\n throw new Error('The identity assertion is malformed');\n }\n\n // The key to check the token with is the one the token names, so it is read unchecked first.\n let claims: Record<string, unknown>;\n try {\n claims = decodeJwt(assertion.token);\n } catch {\n throw new Error('The identity token has no claims');\n }\n const publicKey = claimedKey(claims.cpk);\n try {\n await compactVerify(assertion.token, publicKey, { algorithms: ['ES384'] });\n } catch {\n throw new Error('The identity token is not signed by its own key');\n }\n if (typeof claims.exp === 'number' && claims.exp < nowMs / 1000) throw new Error('The identity token has expired');\n\n const fingerprints = canonicalFingerprints(sdpFingerprints(description));\n if (!(await verifiesDetachedEs384(assertion.fingerprints, fingerprints, publicKey))) {\n throw new Error(\"The identity does not sign this description's fingerprints\");\n }\n\n return {\n publicKey,\n domain: typeof identity.idp.domain === 'string' ? identity.idp.domain : '',\n description: withoutIdentity(description)\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a NetherNet client is told about a server before it joins: the LAN advertisement on UDP
|
|
3
|
+
* 7551 and the `GET /v1/join` answer on the game port both carry these.
|
|
4
|
+
*/
|
|
5
|
+
export interface NetherNetStatus {
|
|
6
|
+
readonly motd: string;
|
|
7
|
+
readonly levelName: string;
|
|
8
|
+
readonly protocol: number;
|
|
9
|
+
/** The game version, such as `1.26.51`. */
|
|
10
|
+
readonly version: string;
|
|
11
|
+
readonly playerCount: number;
|
|
12
|
+
readonly maxPlayers: number;
|
|
13
|
+
/** The `Gametype` id: 0 survival, 1 creative, 2 adventure. */
|
|
14
|
+
readonly gameType: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The `GET /v1/join` body: the status under the field names Bedrock Dedicated Server 1.26.51
|
|
18
|
+
* answers with, in its order.
|
|
19
|
+
*/
|
|
20
|
+
export declare const joinStatusJson: (status: NetherNetStatus) => string;
|
|
21
|
+
/**
|
|
22
|
+
* Reads a `GET /v1/join` body back into the status.
|
|
23
|
+
* @param body - The JSON the server answered with.
|
|
24
|
+
* @throws {Error} When a field is missing or has the wrong type.
|
|
25
|
+
*/
|
|
26
|
+
export declare const decodeJoinStatus: (body: string) => NetherNetStatus;
|
|
27
|
+
//# sourceMappingURL=NetherNetStatus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NetherNetStatus.d.ts","sources":["../src/NetherNetStatus.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,WAAY,eAAe,KAAG,MASnD,CAAC;AAEP;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,SAAU,MAAM,KAAG,eAsB/C,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isRecord } from "./isRecord.js";
|
|
2
|
+
// src/NetherNetStatus.ts
|
|
3
|
+
/**
|
|
4
|
+
* The `GET /v1/join` body: the status under the field names Bedrock Dedicated Server 1.26.51
|
|
5
|
+
* answers with, in its order.
|
|
6
|
+
*/
|
|
7
|
+
var joinStatusJson = (status) => JSON.stringify({
|
|
8
|
+
name: status.motd,
|
|
9
|
+
protocol: status.protocol,
|
|
10
|
+
version: status.version,
|
|
11
|
+
level: status.levelName,
|
|
12
|
+
players: status.playerCount,
|
|
13
|
+
maxPlayers: status.maxPlayers,
|
|
14
|
+
gameType: status.gameType
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Reads a `GET /v1/join` body back into the status.
|
|
18
|
+
* @param body - The JSON the server answered with.
|
|
19
|
+
* @throws {Error} When a field is missing or has the wrong type.
|
|
20
|
+
*/
|
|
21
|
+
var decodeJoinStatus = (body) => {
|
|
22
|
+
const parsed = JSON.parse(body);
|
|
23
|
+
if (!isRecord(parsed)) throw new Error("The join status is not an object");
|
|
24
|
+
const text = (key) => {
|
|
25
|
+
const value = parsed[key];
|
|
26
|
+
if (typeof value !== "string") throw new Error(`The join status has no string ${key}`);
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
const number = (key) => {
|
|
30
|
+
const value = parsed[key];
|
|
31
|
+
if (typeof value !== "number") throw new Error(`The join status has no number ${key}`);
|
|
32
|
+
return value;
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
motd: text("name"),
|
|
36
|
+
protocol: number("protocol"),
|
|
37
|
+
version: text("version"),
|
|
38
|
+
levelName: text("level"),
|
|
39
|
+
playerCount: number("players"),
|
|
40
|
+
maxPlayers: number("maxPlayers"),
|
|
41
|
+
gameType: number("gameType")
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export { decodeJoinStatus, joinStatusJson };
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=NetherNetStatus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NetherNetStatus.js","names":[],"sources":["../src/NetherNetStatus.ts"],"sourcesContent":["import { isRecord } from './isRecord';\n\n/**\n * What a NetherNet client is told about a server before it joins: the LAN advertisement on UDP\n * 7551 and the `GET /v1/join` answer on the game port both carry these.\n */\nexport interface NetherNetStatus {\n readonly motd: string;\n readonly levelName: string;\n readonly protocol: number;\n /** The game version, such as `1.26.51`. */\n readonly version: string;\n readonly playerCount: number;\n readonly maxPlayers: number;\n /** The `Gametype` id: 0 survival, 1 creative, 2 adventure. */\n readonly gameType: number;\n}\n\n/**\n * The `GET /v1/join` body: the status under the field names Bedrock Dedicated Server 1.26.51\n * answers with, in its order.\n */\nexport const joinStatusJson = (status: NetherNetStatus): string =>\n JSON.stringify({\n name: status.motd,\n protocol: status.protocol,\n version: status.version,\n level: status.levelName,\n players: status.playerCount,\n maxPlayers: status.maxPlayers,\n gameType: status.gameType\n });\n\n/**\n * Reads a `GET /v1/join` body back into the status.\n * @param body - The JSON the server answered with.\n * @throws {Error} When a field is missing or has the wrong type.\n */\nexport const decodeJoinStatus = (body: string): NetherNetStatus => {\n const parsed: unknown = JSON.parse(body);\n if (!isRecord(parsed)) throw new Error('The join status is not an object');\n const text = (key: string): string => {\n const value = parsed[key];\n if (typeof value !== 'string') throw new Error(`The join status has no string ${key}`);\n return value;\n };\n const number = (key: string): number => {\n const value = parsed[key];\n if (typeof value !== 'number') throw new Error(`The join status has no number ${key}`);\n return value;\n };\n return {\n motd: text('name'),\n protocol: number('protocol'),\n version: text('version'),\n levelName: text('level'),\n playerCount: number('players'),\n maxPlayers: number('maxPlayers'),\n gameType: number('gameType')\n };\n};\n"],"mappings":";;;;;;AAsBA,IAAa,kBAAkB,WAC3B,KAAK,UAAU;CACX,MAAM,OAAO;CACb,UAAU,OAAO;CACjB,SAAS,OAAO;CAChB,OAAO,OAAO;CACd,SAAS,OAAO;CAChB,YAAY,OAAO;CACnB,UAAU,OAAO;AACrB,CAAC;;;;;;AAOL,IAAa,oBAAoB,SAAkC;CAC/D,MAAM,SAAkB,KAAK,MAAM,IAAI;CACvC,IAAI,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI,MAAM,kCAAkC;CACzE,MAAM,QAAQ,QAAwB;EAClC,MAAM,QAAQ,OAAO;EACrB,IAAI,OAAO,UAAU,UAAU,MAAM,IAAI,MAAM,iCAAiC,KAAK;EACrF,OAAO;CACX;CACA,MAAM,UAAU,QAAwB;EACpC,MAAM,QAAQ,OAAO;EACrB,IAAI,OAAO,UAAU,UAAU,MAAM,IAAI,MAAM,iCAAiC,KAAK;EACrF,OAAO;CACX;CACA,OAAO;EACH,MAAM,KAAK,MAAM;EACjB,UAAU,OAAO,UAAU;EAC3B,SAAS,KAAK,SAAS;EACvB,WAAW,KAAK,OAAO;EACvB,aAAa,OAAO,SAAS;EAC7B,YAAY,OAAO,YAAY;EAC/B,UAAU,OAAO,UAAU;CAC/B;AACJ"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PortRange } from './udpPorts';
|
|
2
|
+
/**
|
|
3
|
+
* Makes every peer connection a `nethernet` `Server` builds take its UDP ports from `range`.
|
|
4
|
+
*
|
|
5
|
+
* `nethernet` 1.1.2 passes nothing but ICE servers to the peer connections it builds, so there is
|
|
6
|
+
* no option to set; this wraps the backend it holds instead. Werift, its default backend, honors
|
|
7
|
+
* `icePortRange`; the optional native backend ignores it. Replace this with an option once
|
|
8
|
+
* `nethernet` takes a peer connection configuration.
|
|
9
|
+
* @param server - The `nethernet` `Server`, before it negotiates any peer.
|
|
10
|
+
* @param range - The ports to allocate from, at least two.
|
|
11
|
+
* @throws {Error} When the server does not hold a backend in the shape this expects.
|
|
12
|
+
*/
|
|
13
|
+
export declare const constrainIcePorts: (server: object, range: PortRange) => void;
|
|
14
|
+
//# sourceMappingURL=constrainIcePorts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constrainIcePorts.d.ts","sources":["../src/constrainIcePorts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAU5C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,WAAY,MAAM,SAAS,SAAS,KAAG,IAYpE,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isRecord } from "./isRecord.js";
|
|
2
|
+
// src/constrainIcePorts.ts
|
|
3
|
+
var isBackend = (value) => isRecord(value) && typeof value.RTCPeerConnection === "function";
|
|
4
|
+
/**
|
|
5
|
+
* Makes every peer connection a `nethernet` `Server` builds take its UDP ports from `range`.
|
|
6
|
+
*
|
|
7
|
+
* `nethernet` 1.1.2 passes nothing but ICE servers to the peer connections it builds, so there is
|
|
8
|
+
* no option to set; this wraps the backend it holds instead. Werift, its default backend, honors
|
|
9
|
+
* `icePortRange`; the optional native backend ignores it. Replace this with an option once
|
|
10
|
+
* `nethernet` takes a peer connection configuration.
|
|
11
|
+
* @param server - The `nethernet` `Server`, before it negotiates any peer.
|
|
12
|
+
* @param range - The ports to allocate from, at least two.
|
|
13
|
+
* @throws {Error} When the server does not hold a backend in the shape this expects.
|
|
14
|
+
*/
|
|
15
|
+
var constrainIcePorts = (server, range) => {
|
|
16
|
+
const backend = Reflect.get(server, "webrtc");
|
|
17
|
+
if (!isBackend(backend)) throw new Error("This nethernet server has no WebRTC backend to give a port range");
|
|
18
|
+
const Base = backend.RTCPeerConnection;
|
|
19
|
+
const icePortRange = [range.start, range.end];
|
|
20
|
+
class RangedPeerConnection extends Base {
|
|
21
|
+
constructor(configuration) {
|
|
22
|
+
super({
|
|
23
|
+
...configuration,
|
|
24
|
+
icePortRange
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
Reflect.set(server, "webrtc", {
|
|
29
|
+
...backend,
|
|
30
|
+
RTCPeerConnection: RangedPeerConnection
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
export { constrainIcePorts };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=constrainIcePorts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constrainIcePorts.js","names":[],"sources":["../src/constrainIcePorts.ts"],"sourcesContent":["import { isRecord } from './isRecord';\nimport type { PortRange } from './udpPorts';\n\n/** The WebRTC backend npm `nethernet` builds peer connections from, as far as this needs it. */\ninterface WebRtcBackend {\n readonly RTCPeerConnection: new (configuration: Record<string, unknown>) => object;\n}\n\nconst isBackend = (value: unknown): value is WebRtcBackend =>\n isRecord(value) && typeof value.RTCPeerConnection === 'function';\n\n/**\n * Makes every peer connection a `nethernet` `Server` builds take its UDP ports from `range`.\n *\n * `nethernet` 1.1.2 passes nothing but ICE servers to the peer connections it builds, so there is\n * no option to set; this wraps the backend it holds instead. Werift, its default backend, honors\n * `icePortRange`; the optional native backend ignores it. Replace this with an option once\n * `nethernet` takes a peer connection configuration.\n * @param server - The `nethernet` `Server`, before it negotiates any peer.\n * @param range - The ports to allocate from, at least two.\n * @throws {Error} When the server does not hold a backend in the shape this expects.\n */\nexport const constrainIcePorts = (server: object, range: PortRange): void => {\n const backend: unknown = Reflect.get(server, 'webrtc');\n if (!isBackend(backend)) throw new Error('This nethernet server has no WebRTC backend to give a port range');\n\n const Base = backend.RTCPeerConnection;\n const icePortRange: [number, number] = [range.start, range.end];\n class RangedPeerConnection extends Base {\n public constructor(configuration: Record<string, unknown>) {\n super({ ...configuration, icePortRange });\n }\n }\n Reflect.set(server, 'webrtc', { ...backend, RTCPeerConnection: RangedPeerConnection });\n};\n"],"mappings":";;AAQA,IAAM,aAAa,UACf,SAAS,KAAK,KAAK,OAAO,MAAM,sBAAsB;;;;;;;;;;;;AAa1D,IAAa,qBAAqB,QAAgB,UAA2B;CACzE,MAAM,UAAmB,QAAQ,IAAI,QAAQ,QAAQ;CACrD,IAAI,CAAC,UAAU,OAAO,GAAG,MAAM,IAAI,MAAM,kEAAkE;CAE3G,MAAM,OAAO,QAAQ;CACrB,MAAM,eAAiC,CAAC,MAAM,OAAO,MAAM,GAAG;CAC9D,MAAM,6BAA6B,KAAK;EACpC,YAAmB,eAAwC;GACvD,MAAM;IAAE,GAAG;IAAe;GAAa,CAAC;EAC5C;CACJ;CACA,QAAQ,IAAI,QAAQ,UAAU;EAAE,GAAG;EAAS,mBAAmB;CAAqB,CAAC;AACzF"}
|
package/dist/es384.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { KeyObject } from 'node:crypto';
|
|
2
|
+
/** The curve Bedrock signs with: the login chain, the encryption handshake and NetherNet identities. */
|
|
3
|
+
export declare const BEDROCK_CURVE = "secp384r1";
|
|
4
|
+
/**
|
|
5
|
+
* A compact ES384 JWS (a JWT, when the payload is a claim set): `header.payload.signature`,
|
|
6
|
+
* signed by `jose`.
|
|
7
|
+
* @param header - Header fields besides `alg`, such as `x5u`.
|
|
8
|
+
* @param payload - The JSON payload.
|
|
9
|
+
* @param privateKey - A P-384 private key.
|
|
10
|
+
*/
|
|
11
|
+
export declare const compactEs384: (header: Readonly<Record<string, unknown>>, payload: unknown, privateKey: KeyObject) => Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* A detached ES384 JWS over exact payload bytes (RFC 7515 appendix F): `header..signature`. The
|
|
14
|
+
* verifier rebuilds the payload itself, so its bytes must be the canonical ones it will build.
|
|
15
|
+
* @param payload - The payload bytes, signed as given.
|
|
16
|
+
* @param privateKey - A P-384 private key.
|
|
17
|
+
*/
|
|
18
|
+
export declare const detachedEs384: (payload: string, privateKey: KeyObject) => Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Whether `jws` is a detached ES384 signature over `payload` by `publicKey`.
|
|
21
|
+
* @param jws - The detached JWS, `header..signature`.
|
|
22
|
+
* @param payload - The payload bytes the signer should have signed.
|
|
23
|
+
* @param publicKey - The P-384 key it should verify against.
|
|
24
|
+
*/
|
|
25
|
+
export declare const verifiesDetachedEs384: (jws: string, payload: string, publicKey: KeyObject) => Promise<boolean>;
|
|
26
|
+
//# sourceMappingURL=es384.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"es384.d.ts","sources":["../src/es384.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,wGAAwG;AACxG,eAAO,MAAM,aAAa,cAAc,CAAC;AAKzC;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,WACb,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,WAChC,OAAO,cACJ,SAAS,KACtB,OAAO,CAAC,MAAM,CAGQ,CAAC;AAE1B;;;;;GAKG;AACH,eAAO,MAAM,aAAa,YAAmB,MAAM,cAAc,SAAS,KAAG,OAAO,CAAC,MAAM,CAG1F,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAe,MAAM,WAAW,MAAM,aAAa,SAAS,KAAG,OAAO,CAAC,OAAO,CAa/G,CAAC"}
|
package/dist/es384.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { CompactSign, FlattenedSign, flattenedVerify } from "jose";
|
|
2
|
+
// src/es384.ts
|
|
3
|
+
/** The curve Bedrock signs with: the login chain, the encryption handshake and NetherNet identities. */
|
|
4
|
+
var BEDROCK_CURVE = "secp384r1";
|
|
5
|
+
var ES384 = "ES384";
|
|
6
|
+
var encoder = new TextEncoder();
|
|
7
|
+
/**
|
|
8
|
+
* A compact ES384 JWS (a JWT, when the payload is a claim set): `header.payload.signature`,
|
|
9
|
+
* signed by `jose`.
|
|
10
|
+
* @param header - Header fields besides `alg`, such as `x5u`.
|
|
11
|
+
* @param payload - The JSON payload.
|
|
12
|
+
* @param privateKey - A P-384 private key.
|
|
13
|
+
*/
|
|
14
|
+
var compactEs384 = (header, payload, privateKey) => new CompactSign(encoder.encode(JSON.stringify(payload))).setProtectedHeader({
|
|
15
|
+
alg: ES384,
|
|
16
|
+
...header
|
|
17
|
+
}).sign(privateKey);
|
|
18
|
+
/**
|
|
19
|
+
* A detached ES384 JWS over exact payload bytes (RFC 7515 appendix F): `header..signature`. The
|
|
20
|
+
* verifier rebuilds the payload itself, so its bytes must be the canonical ones it will build.
|
|
21
|
+
* @param payload - The payload bytes, signed as given.
|
|
22
|
+
* @param privateKey - A P-384 private key.
|
|
23
|
+
*/
|
|
24
|
+
var detachedEs384 = async (payload, privateKey) => {
|
|
25
|
+
const signed = await new FlattenedSign(encoder.encode(payload)).setProtectedHeader({ alg: ES384 }).sign(privateKey);
|
|
26
|
+
return `${signed.protected ?? ""}..${signed.signature}`;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Whether `jws` is a detached ES384 signature over `payload` by `publicKey`.
|
|
30
|
+
* @param jws - The detached JWS, `header..signature`.
|
|
31
|
+
* @param payload - The payload bytes the signer should have signed.
|
|
32
|
+
* @param publicKey - The P-384 key it should verify against.
|
|
33
|
+
*/
|
|
34
|
+
var verifiesDetachedEs384 = async (jws, payload, publicKey) => {
|
|
35
|
+
const [header, detached, signature, ...extra] = jws.split(".");
|
|
36
|
+
if (header === void 0 || detached !== "" || signature === void 0 || extra.length > 0) return false;
|
|
37
|
+
try {
|
|
38
|
+
await flattenedVerify({
|
|
39
|
+
protected: header,
|
|
40
|
+
payload: Buffer.from(payload).toString("base64url"),
|
|
41
|
+
signature
|
|
42
|
+
}, publicKey, { algorithms: [ES384] });
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
export { BEDROCK_CURVE, compactEs384, detachedEs384, verifiesDetachedEs384 };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=es384.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"es384.js","names":[],"sources":["../src/es384.ts"],"sourcesContent":["import type { KeyObject } from 'node:crypto';\n\nimport { CompactSign, FlattenedSign, flattenedVerify } from 'jose';\n\n/** The curve Bedrock signs with: the login chain, the encryption handshake and NetherNet identities. */\nexport const BEDROCK_CURVE = 'secp384r1';\n\nconst ES384 = 'ES384';\nconst encoder = new TextEncoder();\n\n/**\n * A compact ES384 JWS (a JWT, when the payload is a claim set): `header.payload.signature`,\n * signed by `jose`.\n * @param header - Header fields besides `alg`, such as `x5u`.\n * @param payload - The JSON payload.\n * @param privateKey - A P-384 private key.\n */\nexport const compactEs384 = (\n header: Readonly<Record<string, unknown>>,\n payload: unknown,\n privateKey: KeyObject\n): Promise<string> =>\n new CompactSign(encoder.encode(JSON.stringify(payload)))\n .setProtectedHeader({ alg: ES384, ...header })\n .sign(privateKey);\n\n/**\n * A detached ES384 JWS over exact payload bytes (RFC 7515 appendix F): `header..signature`. The\n * verifier rebuilds the payload itself, so its bytes must be the canonical ones it will build.\n * @param payload - The payload bytes, signed as given.\n * @param privateKey - A P-384 private key.\n */\nexport const detachedEs384 = async (payload: string, privateKey: KeyObject): Promise<string> => {\n const signed = await new FlattenedSign(encoder.encode(payload)).setProtectedHeader({ alg: ES384 }).sign(privateKey);\n return `${signed.protected ?? ''}..${signed.signature}`;\n};\n\n/**\n * Whether `jws` is a detached ES384 signature over `payload` by `publicKey`.\n * @param jws - The detached JWS, `header..signature`.\n * @param payload - The payload bytes the signer should have signed.\n * @param publicKey - The P-384 key it should verify against.\n */\nexport const verifiesDetachedEs384 = async (jws: string, payload: string, publicKey: KeyObject): Promise<boolean> => {\n const [header, detached, signature, ...extra] = jws.split('.');\n if (header === undefined || detached !== '' || signature === undefined || extra.length > 0) return false;\n try {\n await flattenedVerify(\n { protected: header, payload: Buffer.from(payload).toString('base64url'), signature },\n publicKey,\n { algorithms: [ES384] }\n );\n return true;\n } catch {\n return false;\n }\n};\n"],"mappings":";;;AAKA,IAAa,gBAAgB;AAE7B,IAAM,QAAQ;AACd,IAAM,UAAU,IAAI,YAAY;;;;;;;;AAShC,IAAa,gBACT,QACA,SACA,eAEA,IAAI,YAAY,QAAQ,OAAO,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,CACnD,mBAAmB;CAAE,KAAK;CAAO,GAAG;AAAO,CAAC,CAAC,CAC7C,KAAK,UAAU;;;;;;;AAQxB,IAAa,gBAAgB,OAAO,SAAiB,eAA2C;CAC5F,MAAM,SAAS,MAAM,IAAI,cAAc,QAAQ,OAAO,OAAO,CAAC,CAAC,CAAC,mBAAmB,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,UAAU;CAClH,OAAO,GAAG,OAAO,aAAa,GAAG,IAAI,OAAO;AAChD;;;;;;;AAQA,IAAa,wBAAwB,OAAO,KAAa,SAAiB,cAA2C;CACjH,MAAM,CAAC,QAAQ,UAAU,WAAW,GAAG,SAAS,IAAI,MAAM,GAAG;CAC7D,IAAI,WAAW,KAAA,KAAa,aAAa,MAAM,cAAc,KAAA,KAAa,MAAM,SAAS,GAAG,OAAO;CACnG,IAAI;EACA,MAAM,gBACF;GAAE,WAAW;GAAQ,SAAS,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,WAAW;GAAG;EAAU,GACpF,WACA,EAAE,YAAY,CAAC,KAAK,EAAE,CAC1B;EACA,OAAO;CACX,QAAQ;EACJ,OAAO;CACX;AACJ"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { BEDROCK_CURVE, compactEs384, detachedEs384, verifiesDetachedEs384 } from './es384';
|
|
2
|
+
export { HttpSignaling } from './HttpSignaling';
|
|
3
|
+
export type { HttpSignalingOptions, NetherNetOfferAnswerer, SignalingLogger } from './HttpSignaling';
|
|
4
|
+
export { decodeLanAdvertisement, encodeLanAdvertisement, LAN_ADVERTISEMENT_VERSION, lanAdvertisementOf } from './LanAdvertisement';
|
|
5
|
+
export type { LanAdvertisement } from './LanAdvertisement';
|
|
6
|
+
export { NETHERNET_LAN_PORT } from './NetherNetDefaults';
|
|
7
|
+
export { canonicalFingerprints, NetherNetIdentity, verifySelfSignedIdentity } from './NetherNetIdentity';
|
|
8
|
+
export type { IdentityKeyFormat, NetherNetIdentityOptions, VerifiedIdentity } from './NetherNetIdentity';
|
|
9
|
+
export { decodeJoinStatus, joinStatusJson } from './NetherNetStatus';
|
|
10
|
+
export { exchangeOffer, JoinRefusedError, probeJoin } from './joinByAddress';
|
|
11
|
+
export type { JoinProbe, SignalingAddress } from './joinByAddress';
|
|
12
|
+
export type { NetherNetStatus } from './NetherNetStatus';
|
|
13
|
+
export { constrainIcePorts } from './constrainIcePorts';
|
|
14
|
+
export { sdpFingerprints, withoutIdentity, withSessionAttribute } from './sdp';
|
|
15
|
+
export { describeCandidates, tracePeerConnection } from './tracePeerConnection';
|
|
16
|
+
export { parseUdpPorts, withMappedCandidates } from './udpPorts';
|
|
17
|
+
export type { PortMapping, PortRange, UdpPorts } from './udpPorts';
|
|
18
|
+
export type { SdpFingerprint } from './sdp';
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAC5F,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACrG,OAAO,EACH,sBAAsB,EACtB,sBAAsB,EACtB,yBAAyB,EACzB,kBAAkB,EACrB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AACzG,YAAY,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACzG,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC7E,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnE,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAC/E,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACjE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACnE,YAAY,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BEDROCK_CURVE, compactEs384, detachedEs384, verifiesDetachedEs384 } from "./es384.js";
|
|
2
|
+
import { HttpSignaling } from "./HttpSignaling.js";
|
|
3
|
+
import { LAN_ADVERTISEMENT_VERSION, decodeLanAdvertisement, encodeLanAdvertisement, lanAdvertisementOf } from "./LanAdvertisement.js";
|
|
4
|
+
import { NETHERNET_LAN_PORT } from "./NetherNetDefaults.js";
|
|
5
|
+
import { NetherNetIdentity, canonicalFingerprints, verifySelfSignedIdentity } from "./NetherNetIdentity.js";
|
|
6
|
+
import { decodeJoinStatus, joinStatusJson } from "./NetherNetStatus.js";
|
|
7
|
+
import { JoinRefusedError, exchangeOffer, probeJoin } from "./joinByAddress.js";
|
|
8
|
+
import { constrainIcePorts } from "./constrainIcePorts.js";
|
|
9
|
+
import { sdpFingerprints, withSessionAttribute, withoutIdentity } from "./sdp.js";
|
|
10
|
+
import { describeCandidates, tracePeerConnection } from "./tracePeerConnection.js";
|
|
11
|
+
import { parseUdpPorts, withMappedCandidates } from "./udpPorts.js";
|
|
12
|
+
export { BEDROCK_CURVE, HttpSignaling, JoinRefusedError, LAN_ADVERTISEMENT_VERSION, NETHERNET_LAN_PORT, NetherNetIdentity, canonicalFingerprints, compactEs384, constrainIcePorts, decodeJoinStatus, decodeLanAdvertisement, describeCandidates, detachedEs384, encodeLanAdvertisement, exchangeOffer, joinStatusJson, lanAdvertisementOf, parseUdpPorts, probeJoin, sdpFingerprints, tracePeerConnection, verifiesDetachedEs384, verifySelfSignedIdentity, withMappedCandidates, withSessionAttribute, withoutIdentity };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isRecord.d.ts","sources":["../src/isRecord.ts"],"names":[],"mappings":"AAAA,4GAA4G;AAC5G,eAAO,MAAM,QAAQ,UAAW,OAAO,KAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CACD,CAAC"}
|
package/dist/isRecord.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// src/isRecord.ts
|
|
2
|
+
/** Whether a value is a non-null object, which is what parsed JSON has to be before its fields are read. */
|
|
3
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
export { isRecord };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=isRecord.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isRecord.js","names":[],"sources":["../src/isRecord.ts"],"sourcesContent":["/** Whether a value is a non-null object, which is what parsed JSON has to be before its fields are read. */\nexport const isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n"],"mappings":";;AACA,IAAa,YAAY,UACrB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { NetherNetStatus } from './NetherNetStatus';
|
|
2
|
+
/** Where a NetherNet server signals: the TCP side of its game port. */
|
|
3
|
+
export interface SignalingAddress {
|
|
4
|
+
readonly host: string;
|
|
5
|
+
readonly port: number;
|
|
6
|
+
}
|
|
7
|
+
/** A failed `POST /v1/join/{networkId}`, with what the server answered. */
|
|
8
|
+
export declare class JoinRefusedError extends Error {
|
|
9
|
+
readonly status: number;
|
|
10
|
+
readonly body: string;
|
|
11
|
+
constructor(status: number, body: string);
|
|
12
|
+
}
|
|
13
|
+
/** What `GET /v1/join` said about a server that speaks NetherNet. */
|
|
14
|
+
export interface JoinProbe {
|
|
15
|
+
/**
|
|
16
|
+
* The server's status, when it sent one. Bedrock Dedicated Server 1.26.51 answers with an empty
|
|
17
|
+
* body when LAN visibility is off, and go-nethernet does when it has no status to give.
|
|
18
|
+
*/
|
|
19
|
+
readonly status?: NetherNetStatus;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Asks whether the server speaks NetherNet, as a client does before it joins by address: any 2xx
|
|
23
|
+
* from `GET /v1/join` means yes, whatever the body says.
|
|
24
|
+
* @param address - The server's host and game port.
|
|
25
|
+
* @param signal - Aborts the request.
|
|
26
|
+
* @returns What the server said, or undefined when it does not speak NetherNet there.
|
|
27
|
+
* @throws {Error} When a 2xx body is present but is not a join status.
|
|
28
|
+
*/
|
|
29
|
+
export declare const probeJoin: (address: SignalingAddress, signal?: AbortSignal) => Promise<JoinProbe | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* Trades a complete SDP offer for the server's complete answer, the one request a client sends
|
|
32
|
+
* per attempt.
|
|
33
|
+
* @param address - The server's host and game port.
|
|
34
|
+
* @param networkId - The client's own network ID.
|
|
35
|
+
* @param offer - The offer, with every candidate and the client's `a=identity`.
|
|
36
|
+
* @param signal - Aborts the request.
|
|
37
|
+
* @returns The answer.
|
|
38
|
+
* @throws {JoinRefusedError} When the server answers anything but 2xx.
|
|
39
|
+
*/
|
|
40
|
+
export declare const exchangeOffer: (address: SignalingAddress, networkId: bigint, offer: string, signal?: AbortSignal) => Promise<string>;
|
|
41
|
+
//# sourceMappingURL=joinByAddress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"joinByAddress.d.ts","sources":["../src/joinByAddress.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACzB;AAED,2EAA2E;AAC3E,qBAAa,gBAAiB,SAAQ,KAAK;aAEnB,MAAM,EAAE,MAAM;aACd,IAAI,EAAE,MAAM;IAFhC,YACoB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EAG/B;CACJ;AAMD,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACtB;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,YAAmB,gBAAgB,WAAW,WAAW,KAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAK9G,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,YACb,gBAAgB,aACd,MAAM,SACV,MAAM,WACJ,WAAW,KACrB,OAAO,CAAC,MAAM,CAUhB,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { decodeJoinStatus } from "./NetherNetStatus.js";
|
|
2
|
+
// src/joinByAddress.ts
|
|
3
|
+
/** A failed `POST /v1/join/{networkId}`, with what the server answered. */
|
|
4
|
+
var JoinRefusedError = class extends Error {
|
|
5
|
+
status;
|
|
6
|
+
body;
|
|
7
|
+
constructor(status, body) {
|
|
8
|
+
super(`The server refused the offer with HTTP ${status}${body === "" ? "" : `: ${body}`}`);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.body = body;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
/** The signaling origin, with an IPv6 host in brackets as a URL needs. */
|
|
14
|
+
var origin = ({ host, port }) => `http://${host.includes(":") ? `[${host}]` : host}:${port}`;
|
|
15
|
+
/**
|
|
16
|
+
* Asks whether the server speaks NetherNet, as a client does before it joins by address: any 2xx
|
|
17
|
+
* from `GET /v1/join` means yes, whatever the body says.
|
|
18
|
+
* @param address - The server's host and game port.
|
|
19
|
+
* @param signal - Aborts the request.
|
|
20
|
+
* @returns What the server said, or undefined when it does not speak NetherNet there.
|
|
21
|
+
* @throws {Error} When a 2xx body is present but is not a join status.
|
|
22
|
+
*/
|
|
23
|
+
var probeJoin = async (address, signal) => {
|
|
24
|
+
const response = await fetch(`${origin(address)}/v1/join`, { signal });
|
|
25
|
+
if (!response.ok) return void 0;
|
|
26
|
+
const body = await response.text();
|
|
27
|
+
return body.trim() === "" ? {} : { status: decodeJoinStatus(body) };
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Trades a complete SDP offer for the server's complete answer, the one request a client sends
|
|
31
|
+
* per attempt.
|
|
32
|
+
* @param address - The server's host and game port.
|
|
33
|
+
* @param networkId - The client's own network ID.
|
|
34
|
+
* @param offer - The offer, with every candidate and the client's `a=identity`.
|
|
35
|
+
* @param signal - Aborts the request.
|
|
36
|
+
* @returns The answer.
|
|
37
|
+
* @throws {JoinRefusedError} When the server answers anything but 2xx.
|
|
38
|
+
*/
|
|
39
|
+
var exchangeOffer = async (address, networkId, offer, signal) => {
|
|
40
|
+
const response = await fetch(`${origin(address)}/v1/join/${networkId}`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "Content-Type": "application/sdp" },
|
|
43
|
+
body: offer,
|
|
44
|
+
signal
|
|
45
|
+
});
|
|
46
|
+
const body = await response.text();
|
|
47
|
+
if (!response.ok) throw new JoinRefusedError(response.status, body);
|
|
48
|
+
return body;
|
|
49
|
+
};
|
|
50
|
+
export { JoinRefusedError, exchangeOffer, probeJoin };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=joinByAddress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"joinByAddress.js","names":[],"sources":["../src/joinByAddress.ts"],"sourcesContent":["import { decodeJoinStatus } from './NetherNetStatus';\nimport type { NetherNetStatus } from './NetherNetStatus';\n\n/** Where a NetherNet server signals: the TCP side of its game port. */\nexport interface SignalingAddress {\n readonly host: string;\n readonly port: number;\n}\n\n/** A failed `POST /v1/join/{networkId}`, with what the server answered. */\nexport class JoinRefusedError extends Error {\n public constructor(\n public readonly status: number,\n public readonly body: string\n ) {\n super(`The server refused the offer with HTTP ${status}${body === '' ? '' : `: ${body}`}`);\n }\n}\n\n/** The signaling origin, with an IPv6 host in brackets as a URL needs. */\nconst origin = ({ host, port }: SignalingAddress): string =>\n `http://${host.includes(':') ? `[${host}]` : host}:${port}`;\n\n/** What `GET /v1/join` said about a server that speaks NetherNet. */\nexport interface JoinProbe {\n /**\n * The server's status, when it sent one. Bedrock Dedicated Server 1.26.51 answers with an empty\n * body when LAN visibility is off, and go-nethernet does when it has no status to give.\n */\n readonly status?: NetherNetStatus;\n}\n\n/**\n * Asks whether the server speaks NetherNet, as a client does before it joins by address: any 2xx\n * from `GET /v1/join` means yes, whatever the body says.\n * @param address - The server's host and game port.\n * @param signal - Aborts the request.\n * @returns What the server said, or undefined when it does not speak NetherNet there.\n * @throws {Error} When a 2xx body is present but is not a join status.\n */\nexport const probeJoin = async (address: SignalingAddress, signal?: AbortSignal): Promise<JoinProbe | undefined> => {\n const response = await fetch(`${origin(address)}/v1/join`, { signal });\n if (!response.ok) return undefined;\n const body = await response.text();\n return body.trim() === '' ? {} : { status: decodeJoinStatus(body) };\n};\n\n/**\n * Trades a complete SDP offer for the server's complete answer, the one request a client sends\n * per attempt.\n * @param address - The server's host and game port.\n * @param networkId - The client's own network ID.\n * @param offer - The offer, with every candidate and the client's `a=identity`.\n * @param signal - Aborts the request.\n * @returns The answer.\n * @throws {JoinRefusedError} When the server answers anything but 2xx.\n */\nexport const exchangeOffer = async (\n address: SignalingAddress,\n networkId: bigint,\n offer: string,\n signal?: AbortSignal\n): Promise<string> => {\n const response = await fetch(`${origin(address)}/v1/join/${networkId}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/sdp' },\n body: offer,\n signal\n });\n const body = await response.text();\n if (!response.ok) throw new JoinRefusedError(response.status, body);\n return body;\n};\n"],"mappings":";;;AAUA,IAAa,mBAAb,cAAsC,MAAM;CAEpB;CACA;CAFpB,YACI,QACA,MACF;EACE,MAAM,0CAA0C,SAAS,SAAS,KAAK,KAAK,KAAK,QAAQ;EAHzE,KAAA,SAAA;EACA,KAAA,OAAA;CAGpB;AACJ;;AAGA,IAAM,UAAU,EAAE,MAAM,WACpB,UAAU,KAAK,SAAS,GAAG,IAAI,IAAI,KAAK,KAAK,KAAK,GAAG;;;;;;;;;AAmBzD,IAAa,YAAY,OAAO,SAA2B,WAAyD;CAChH,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC;CACrE,IAAI,CAAC,SAAS,IAAI,OAAO,KAAA;CACzB,MAAM,OAAO,MAAM,SAAS,KAAK;CACjC,OAAO,KAAK,KAAK,MAAM,KAAK,CAAC,IAAI,EAAE,QAAQ,iBAAiB,IAAI,EAAE;AACtE;;;;;;;;;;;AAYA,IAAa,gBAAgB,OACzB,SACA,WACA,OACA,WACkB;CAClB,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,OAAO,EAAE,WAAW,aAAa;EACpE,QAAQ;EACR,SAAS,EAAE,gBAAgB,kBAAkB;EAC7C,MAAM;EACN;CACJ,CAAC;CACD,MAAM,OAAO,MAAM,SAAS,KAAK;CACjC,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,iBAAiB,SAAS,QAAQ,IAAI;CAClE,OAAO;AACX"}
|
package/dist/sdp.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** One `a=fingerprint` attribute: the DTLS certificate hash the peer must present. */
|
|
2
|
+
export interface SdpFingerprint {
|
|
3
|
+
/** The hash function, such as `sha-256`. */
|
|
4
|
+
readonly algorithm: string;
|
|
5
|
+
/** The hash, colon-separated hex, as the SDP spells it. */
|
|
6
|
+
readonly digest: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Every `a=fingerprint` in a description, in the order they appear.
|
|
10
|
+
* @param sdp - The session description.
|
|
11
|
+
*/
|
|
12
|
+
export declare const sdpFingerprints: (sdp: string) => SdpFingerprint[];
|
|
13
|
+
/**
|
|
14
|
+
* The description with every `a=identity` line removed. The identity is NetherNet's, checked
|
|
15
|
+
* before the description reaches WebRTC, which does not know the attribute.
|
|
16
|
+
* @param sdp - The session description.
|
|
17
|
+
*/
|
|
18
|
+
export declare const withoutIdentity: (sdp: string) => string;
|
|
19
|
+
/**
|
|
20
|
+
* The description with one session-level attribute added: after every other session-level line
|
|
21
|
+
* and before the first media section, which is where NetherNet expects `a=identity`.
|
|
22
|
+
* @param sdp - The session description.
|
|
23
|
+
* @param attribute - The attribute line, such as `a=identity:…`.
|
|
24
|
+
*/
|
|
25
|
+
export declare const withSessionAttribute: (sdp: string, attribute: string) => string;
|
|
26
|
+
//# sourceMappingURL=sdp.d.ts.map
|