@honeybbq/teamspeak-client 0.3.1 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +322 -60
  2. package/dist/abort.d.ts +5 -0
  3. package/dist/abort.d.ts.map +1 -0
  4. package/dist/address.d.ts +8 -0
  5. package/dist/address.d.ts.map +1 -1
  6. package/dist/api/voice.d.ts +24 -0
  7. package/dist/api/voice.d.ts.map +1 -0
  8. package/dist/capabilities.d.ts +62 -0
  9. package/dist/capabilities.d.ts.map +1 -0
  10. package/dist/client.d.ts +172 -39
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/commands.d.ts +45 -27
  13. package/dist/commands.d.ts.map +1 -1
  14. package/dist/crypto/crypt.d.ts +19 -4
  15. package/dist/crypto/crypt.d.ts.map +1 -1
  16. package/dist/crypto/identity.d.ts +87 -8
  17. package/dist/crypto/identity.d.ts.map +1 -1
  18. package/dist/crypto/index.d.ts +2 -1
  19. package/dist/crypto/index.d.ts.map +1 -1
  20. package/dist/diagnostics.d.ts +45 -0
  21. package/dist/diagnostics.d.ts.map +1 -0
  22. package/dist/discovery/resolver.d.ts +21 -3
  23. package/dist/discovery/resolver.d.ts.map +1 -1
  24. package/dist/errors.d.ts +70 -16
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/events.d.ts +4 -5
  27. package/dist/events.d.ts.map +1 -1
  28. package/dist/handshake/crypt-handshake.d.ts +1 -0
  29. package/dist/handshake/crypt-handshake.d.ts.map +1 -1
  30. package/dist/handshake/crypt-init2.d.ts.map +1 -1
  31. package/dist/handshake/license.d.ts +7 -0
  32. package/dist/handshake/license.d.ts.map +1 -1
  33. package/dist/handshake.d.ts +14 -5
  34. package/dist/handshake.d.ts.map +1 -1
  35. package/dist/helpers.d.ts +0 -8
  36. package/dist/helpers.d.ts.map +1 -1
  37. package/dist/index.d.ts +20 -6
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +3967 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/notifications.d.ts +7 -15
  42. package/dist/notifications.d.ts.map +1 -1
  43. package/dist/reconnect.d.ts +69 -0
  44. package/dist/reconnect.d.ts.map +1 -0
  45. package/dist/throttle.d.ts +9 -0
  46. package/dist/throttle.d.ts.map +1 -1
  47. package/dist/transfer.d.ts +27 -25
  48. package/dist/transfer.d.ts.map +1 -1
  49. package/dist/transport/handler.d.ts +37 -5
  50. package/dist/transport/handler.d.ts.map +1 -1
  51. package/dist/transport/quicklz.d.ts +19 -0
  52. package/dist/transport/quicklz.d.ts.map +1 -1
  53. package/dist/types.d.ts +277 -97
  54. package/dist/types.d.ts.map +1 -1
  55. package/package.json +21 -56
  56. package/src/abort.ts +24 -0
  57. package/src/address.ts +74 -0
  58. package/src/api/voice.ts +61 -0
  59. package/src/capabilities.ts +92 -0
  60. package/src/client.ts +1392 -0
  61. package/src/commands.ts +193 -0
  62. package/src/crypto/crypt-ops.ts +6 -0
  63. package/src/crypto/crypt.ts +274 -0
  64. package/src/crypto/eax.ts +152 -0
  65. package/src/crypto/identity.ts +484 -0
  66. package/src/crypto/index.ts +21 -0
  67. package/src/crypto/primitives.ts +141 -0
  68. package/src/diagnostics.ts +58 -0
  69. package/src/discovery/index.ts +1 -0
  70. package/src/discovery/resolver.ts +272 -0
  71. package/src/errors.ts +156 -0
  72. package/src/events.ts +28 -0
  73. package/src/handshake/crypt-handshake.ts +135 -0
  74. package/src/handshake/crypt-init2.ts +48 -0
  75. package/src/handshake/index.ts +3 -0
  76. package/src/handshake/license.test.ts +49 -0
  77. package/src/handshake/license.ts +208 -0
  78. package/src/handshake.ts +136 -0
  79. package/src/helpers.ts +10 -0
  80. package/src/index.ts +74 -0
  81. package/src/notifications.ts +136 -0
  82. package/src/reconnect.ts +141 -0
  83. package/src/throttle.ts +92 -0
  84. package/src/transfer.ts +127 -0
  85. package/src/transport/generation-window.ts +77 -0
  86. package/src/transport/handler.ts +914 -0
  87. package/src/transport/index.ts +14 -0
  88. package/src/transport/packet.ts +69 -0
  89. package/src/transport/quicklz.ts +209 -0
  90. package/src/types.ts +349 -0
  91. package/dist/address-BR-5unVo.cjs +0 -2
  92. package/dist/address-BR-5unVo.cjs.map +0 -1
  93. package/dist/address-BfmovVpp.js +0 -65
  94. package/dist/address-BfmovVpp.js.map +0 -1
  95. package/dist/api.d.ts +0 -17
  96. package/dist/api.d.ts.map +0 -1
  97. package/dist/command/command.d.ts +0 -12
  98. package/dist/command/command.d.ts.map +0 -1
  99. package/dist/command/index.cjs +0 -1
  100. package/dist/command/index.d.ts +0 -4
  101. package/dist/command/index.d.ts.map +0 -1
  102. package/dist/command/index.mjs +0 -3
  103. package/dist/command/parser.d.ts +0 -3
  104. package/dist/command/parser.d.ts.map +0 -1
  105. package/dist/command-BLe-C2L2.js +0 -74
  106. package/dist/command-BLe-C2L2.js.map +0 -1
  107. package/dist/command-DAK4pUBP.cjs +0 -4
  108. package/dist/command-DAK4pUBP.cjs.map +0 -1
  109. package/dist/crypt-handshake-CQkQreLx.cjs +0 -2
  110. package/dist/crypt-handshake-CQkQreLx.cjs.map +0 -1
  111. package/dist/crypt-handshake-DziOY4Lq.js +0 -55
  112. package/dist/crypt-handshake-DziOY4Lq.js.map +0 -1
  113. package/dist/crypt-init2-D70ha4LB.js +0 -158
  114. package/dist/crypt-init2-D70ha4LB.js.map +0 -1
  115. package/dist/crypt-init2-DparLekm.cjs +0 -2
  116. package/dist/crypt-init2-DparLekm.cjs.map +0 -1
  117. package/dist/crypto/index.cjs +0 -1
  118. package/dist/crypto/index.mjs +0 -3
  119. package/dist/crypto-Bg02m4ZE.js +0 -165
  120. package/dist/crypto-Bg02m4ZE.js.map +0 -1
  121. package/dist/crypto-CDlweUL3.cjs +0 -2
  122. package/dist/crypto-CDlweUL3.cjs.map +0 -1
  123. package/dist/discovery/index.cjs +0 -1
  124. package/dist/discovery/index.mjs +0 -2
  125. package/dist/handler-DI_abe9c.js +0 -483
  126. package/dist/handler-DI_abe9c.js.map +0 -1
  127. package/dist/handler-DuwoO5RC.cjs +0 -2
  128. package/dist/handler-DuwoO5RC.cjs.map +0 -1
  129. package/dist/handshake/index.cjs +0 -1
  130. package/dist/handshake/index.mjs +0 -3
  131. package/dist/index.cjs +0 -4
  132. package/dist/index.cjs.map +0 -1
  133. package/dist/index.mjs +0 -827
  134. package/dist/index.mjs.map +0 -1
  135. package/dist/parser-CL33R9tr.cjs +0 -2
  136. package/dist/parser-CL33R9tr.cjs.map +0 -1
  137. package/dist/parser-DZMv3Jm-.js +0 -33
  138. package/dist/parser-DZMv3Jm-.js.map +0 -1
  139. package/dist/primitives-cNNAeFCo.cjs +0 -2
  140. package/dist/primitives-cNNAeFCo.cjs.map +0 -1
  141. package/dist/primitives-xIr6Tjk4.js +0 -1457
  142. package/dist/primitives-xIr6Tjk4.js.map +0 -1
  143. package/dist/resolver-6B8dI9gr.js +0 -141
  144. package/dist/resolver-6B8dI9gr.js.map +0 -1
  145. package/dist/resolver-DCcmyWSK.cjs +0 -4
  146. package/dist/resolver-DCcmyWSK.cjs.map +0 -1
  147. package/dist/transport/index.cjs +0 -1
  148. package/dist/transport/index.mjs +0 -2
package/dist/index.js ADDED
@@ -0,0 +1,3967 @@
1
+ import { CommandTimeoutError, ErrorCode, NotConnectedError, Reason, ServerError, TeamspeakError, asChannelID, asClientID, asUID, noopLogger } from "@honeybbq/teamspeak-core";
2
+ import { createCipheriv, createECDH, createHash, createPrivateKey, createPublicKey, createSign, createVerify, generateKeyPairSync, randomBytes, timingSafeEqual } from "node:crypto";
3
+ import { ed25519 } from "@noble/curves/ed25519.js";
4
+ import { lookup, resolveSrv } from "node:dns/promises";
5
+ import { connect, createConnection, isIP } from "node:net";
6
+ import { createSocket } from "node:dgram";
7
+ import { buildCommand, buildCommandOrdered, encodeRequest, parseClient, parseCommand, parseErrorCode, parseIconID, parseInt10, parseServerError, parseUint16, parseUint64, responseSpec, sleep, splitCommandRows } from "@honeybbq/teamspeak-core/internal";
8
+ import { pipeline } from "node:stream/promises";
9
+ import { channel, tracingChannel } from "node:diagnostics_channel";
10
+ export * from "@honeybbq/teamspeak-core";
11
+ /** Where a client is in its connection's life. */
12
+ var ClientStatus = /* @__PURE__ */ Object.freeze({
13
+ /** Not connected: before `connect()`, after `disconnect()`, or after a drop. */
14
+ Disconnected: "disconnected",
15
+ /** Connecting: `connect()` is waiting for the handshake. */
16
+ Connecting: "connecting",
17
+ /** Connected: the handshake completed and commands can be sent. */
18
+ Connected: "connected"
19
+ });
20
+ //#endregion
21
+ //#region src/errors.ts
22
+ /** `connect()` was called on a client that is connecting or connected. */
23
+ var AlreadyConnectedError = class extends TeamspeakError {
24
+ /** {@inheritDoc TeamspeakError.kind} */
25
+ static kind = "AlreadyConnectedError";
26
+ constructor() {
27
+ super("already connecting or connected");
28
+ this.name = "AlreadyConnectedError";
29
+ }
30
+ };
31
+ /**
32
+ * The address a client was given cannot be connected to: no host, a host with
33
+ * characters no host name or IP address has, or a port outside 1–65535.
34
+ * `connect()` rejects with it before it starts, and the client stays
35
+ * disconnected. A name that does not resolve is not this: that fails with a
36
+ * TeamspeakError whose `cause` is the lookup's error, since DNS failures are
37
+ * often transient.
38
+ */
39
+ var InvalidAddressError = class extends TeamspeakError {
40
+ /** {@inheritDoc TeamspeakError.kind} */
41
+ static kind = "InvalidAddressError";
42
+ /** The address as given. */
43
+ address;
44
+ constructor(address, reason) {
45
+ super(`invalid address ${JSON.stringify(address)}: ${reason}`);
46
+ this.name = "InvalidAddressError";
47
+ this.address = address;
48
+ }
49
+ };
50
+ /**
51
+ * A packet failed authentication: it was corrupted, or not from the server.
52
+ * Internal: the packet is dropped, and this never reaches a caller.
53
+ */
54
+ var EAXTagMismatchError = class extends TeamspeakError {
55
+ /** {@inheritDoc TeamspeakError.kind} */
56
+ static kind = "EAXTagMismatchError";
57
+ constructor() {
58
+ super("EAX tag mismatch");
59
+ this.name = "EAXTagMismatchError";
60
+ }
61
+ };
62
+ /**
63
+ * An unencrypted packet failed its integrity check. Internal: the packet is
64
+ * dropped, and this never reaches a caller.
65
+ */
66
+ var FakeSignatureMismatchError = class extends TeamspeakError {
67
+ /** {@inheritDoc TeamspeakError.kind} */
68
+ static kind = "FakeSignatureMismatchError";
69
+ constructor() {
70
+ super("fake signature mismatch");
71
+ this.name = "FakeSignatureMismatchError";
72
+ }
73
+ };
74
+ /** A file transfer connection failed: it could not be opened, or broke during the transfer. */
75
+ var FileTransferError = class extends TeamspeakError {
76
+ /** {@inheritDoc TeamspeakError.kind} */
77
+ static kind = "FileTransferError";
78
+ constructor(message, options) {
79
+ super(message, options);
80
+ this.name = "FileTransferError";
81
+ }
82
+ };
83
+ /**
84
+ * The handshake failed on what the server sent: a malformed or out-of-range
85
+ * Init1 step, public key, license or proof. `connect()` rejects with it, the
86
+ * underlying failure as its `cause`.
87
+ */
88
+ var CryptoInitError = class extends TeamspeakError {
89
+ /** {@inheritDoc TeamspeakError.kind} */
90
+ static kind = "CryptoInitError";
91
+ constructor(message, options) {
92
+ super(message, options);
93
+ this.name = "CryptoInitError";
94
+ }
95
+ };
96
+ /** An identity string `parseIdentity()` cannot read. */
97
+ var InvalidIdentityError = class extends TeamspeakError {
98
+ /** {@inheritDoc TeamspeakError.kind} */
99
+ static kind = "InvalidIdentityError";
100
+ constructor(message = "invalid identity format", options) {
101
+ super(message, options);
102
+ this.name = "InvalidIdentityError";
103
+ }
104
+ };
105
+ /**
106
+ * The server stopped answering: nothing arrived, or a packet went
107
+ * unacknowledged, for a minute.
108
+ */
109
+ var ConnectionTimeoutError = class extends TeamspeakError {
110
+ /** {@inheritDoc TeamspeakError.kind} */
111
+ static kind = "ConnectionTimeoutError";
112
+ constructor() {
113
+ super("connection timed out: the server stopped answering");
114
+ this.name = "ConnectionTimeoutError";
115
+ }
116
+ };
117
+ /**
118
+ * The server ended this client's session: a kick, a ban, or the server
119
+ * stopping. The default reconnect policy retries a server that stopped, but
120
+ * not a kick or a ban.
121
+ */
122
+ var RemovedError = class extends TeamspeakError {
123
+ /** {@inheritDoc TeamspeakError.kind} */
124
+ static kind = "RemovedError";
125
+ /** Why: `Reason.KickServer`, `KickServerBan`, `ServerStop` or `ServerShutdown`. */
126
+ reason;
127
+ /** The reason the server or the invoker gave, if any. */
128
+ reasonMessage;
129
+ /** Who removed the client, for a kick or a ban. */
130
+ invokerName;
131
+ constructor(reason, reasonMessage, invokerName) {
132
+ let message = `removed from the server: ${reasonName(reason)}`;
133
+ if (invokerName) message += ` by ${invokerName}`;
134
+ if (reasonMessage) message += `: ${reasonMessage}`;
135
+ super(message);
136
+ this.name = "RemovedError";
137
+ this.reason = reason;
138
+ this.reasonMessage = reasonMessage;
139
+ this.invokerName = invokerName;
140
+ }
141
+ };
142
+ function reasonName(reason) {
143
+ switch (reason) {
144
+ case Reason.KickServer: return "kicked from server";
145
+ case Reason.KickServerBan: return "banned from server";
146
+ case Reason.ServerStop: return "server stopped";
147
+ case Reason.ServerShutdown: return "server shut down";
148
+ default: return `reason ${reason}`;
149
+ }
150
+ }
151
+ //#endregion
152
+ //#region src/abort.ts
153
+ /** The reason an aborted signal gives, as an error. */
154
+ function abortReason(signal) {
155
+ return signal.reason ?? new DOMException("This operation was aborted", "AbortError");
156
+ }
157
+ /** Settle with `promise`, or reject as soon as `signal` aborts, whichever is first. */
158
+ function abortable(promise, signal) {
159
+ if (!signal) return promise;
160
+ if (signal.aborted) return Promise.reject(abortReason(signal));
161
+ return new Promise((resolve, reject) => {
162
+ const onAbort = () => reject(abortReason(signal));
163
+ signal.addEventListener("abort", onAbort, { once: true });
164
+ promise.then((value) => {
165
+ signal.removeEventListener("abort", onAbort);
166
+ resolve(value);
167
+ }, (error) => {
168
+ signal.removeEventListener("abort", onAbort);
169
+ reject(error);
170
+ });
171
+ });
172
+ }
173
+ //#endregion
174
+ //#region src/crypto/identity.ts
175
+ var P256_SCALAR_SIZE = 32;
176
+ var P256_UNCOMPRESSED_KEY_SIZE = 65;
177
+ var P256_POINT_PREFIX = 4;
178
+ /** The most a security level can be: SHA-1 is 160 bits wide. */
179
+ var MAX_SECURITY_LEVEL = 160;
180
+ var createIdentity;
181
+ var privateKeyOf;
182
+ /**
183
+ * A TeamSpeak client identity: a P-256 key pair plus a counter, the offset,
184
+ * whose hash with the public key sets the identity's security level.
185
+ *
186
+ * Immutable: raising the level returns a new identity. Opaque: the private
187
+ * key is never exposed, and `toString()`, `toJSON()` and `util.inspect()`
188
+ * describe the identity without it, so an identity that reaches a log or an
189
+ * error message does not leak; `export()` is the one way to get the secret
190
+ * form, for storing it. Make one with `generateIdentity()` or
191
+ * `parseIdentity()`.
192
+ */
193
+ var Identity = class Identity {
194
+ static {
195
+ createIdentity = (privateKey, offset) => new Identity(privateKey, offset);
196
+ privateKeyOf = (identity) => identity.#privateKey;
197
+ }
198
+ #privateKey;
199
+ #offset;
200
+ #publicKey;
201
+ constructor(privateKey, offset) {
202
+ this.#privateKey = privateKey;
203
+ this.#offset = offset;
204
+ }
205
+ /** The counter whose hash with the public key sets the security level. */
206
+ get offset() {
207
+ return this.#offset;
208
+ }
209
+ /** The public key in TeamSpeak's encoding: base64 of its DER form. */
210
+ publicKey() {
211
+ if (this.#publicKey !== void 0) return this.#publicKey;
212
+ const rawJwk = this.#privateKey.export({ format: "jwk" });
213
+ if (typeof rawJwk.x !== "string" || typeof rawJwk.y !== "string") return "";
214
+ const xBytes = base64UrlToBytes$1(rawJwk.x, P256_SCALAR_SIZE);
215
+ const yBytes = base64UrlToBytes$1(rawJwk.y, P256_SCALAR_SIZE);
216
+ this.#publicKey = encodeTsPublicKey(xBytes, yBytes);
217
+ return this.#publicKey;
218
+ }
219
+ /**
220
+ * The identity's unique identifier, hashed with `algorithm`: SHA-1 for a
221
+ * TeamSpeak 3 server, SHA-256 for TeamSpeak 6. A connected client's `uid`
222
+ * already uses the one its server does.
223
+ */
224
+ uid(algorithm) {
225
+ return uidFromPublicKey(this.publicKey(), algorithm);
226
+ }
227
+ /**
228
+ * The identity in the form `parseIdentity()` reads, private key included.
229
+ * Store it as you would a password.
230
+ */
231
+ export() {
232
+ const jwk = this.#privateKey.export({ format: "jwk" });
233
+ if (!jwk.d) return `:${this.#offset}`;
234
+ const dBytes = base64UrlToBytes$1(jwk.d, P256_SCALAR_SIZE);
235
+ return `${Buffer.from(dBytes).toString("base64")}:${this.#offset}`;
236
+ }
237
+ /** Describes the identity, by its SHA-1 unique identifier, without its private key. */
238
+ toString() {
239
+ return `identity ${this.uid("sha1")} (private key redacted)`;
240
+ }
241
+ /** The same as `toString()`, so serialising an identity cannot leak its key. */
242
+ toJSON() {
243
+ return this.toString();
244
+ }
245
+ /** The security level: trailing zero bits in SHA-1 of the public key and offset. */
246
+ securityLevel() {
247
+ return levelAt(this.publicKey(), this.#offset);
248
+ }
249
+ /**
250
+ * The same key at an offset whose security level reaches `targetLevel`, as
251
+ * a new identity; this one is unchanged, and returned as it is when it
252
+ * already reaches the level.
253
+ *
254
+ * Each level doubles the expected work, so a high level can take a long
255
+ * time; `signal` bounds it, and the event loop keeps running meanwhile. A
256
+ * level above 160, the width of SHA-1, can never be reached and is refused
257
+ * with a RangeError.
258
+ */
259
+ async upgradeToLevel(targetLevel, options = {}) {
260
+ checkLevel(targetLevel);
261
+ const { signal } = options;
262
+ const prefix = this.publicKey();
263
+ let offset = this.#offset;
264
+ while (true) {
265
+ signal?.throwIfAborted();
266
+ if (levelAt(prefix, offset) >= targetLevel) return offset === this.#offset ? this : new Identity(this.#privateKey, offset);
267
+ offset++;
268
+ if (offset % 10000n === 0n) await new Promise((resolve) => setImmediate(resolve));
269
+ }
270
+ }
271
+ };
272
+ Object.defineProperty(Identity.prototype, Symbol.for("nodejs.util.inspect.custom"), { value() {
273
+ return `Identity <${this.toString()}>`;
274
+ } });
275
+ /**
276
+ * The private key of an identity, for the handshake. Not exported from the
277
+ * package.
278
+ *
279
+ * @internal
280
+ */
281
+ function identityPrivateKey(identity) {
282
+ return privateKeyOf(identity);
283
+ }
284
+ function checkLevel(level) {
285
+ if (!Number.isInteger(level) || level < 0 || level > MAX_SECURITY_LEVEL) throw new RangeError(`a security level is an integer from 0 to 160, not ${level}`);
286
+ }
287
+ function levelAt(publicKey, offset) {
288
+ const h = createHash("sha1");
289
+ h.update(publicKey);
290
+ h.update(offset.toString(10));
291
+ return countLeadingZeros(h.digest());
292
+ }
293
+ /** The largest offset, an unsigned 64-bit counter. */
294
+ var MAX_UINT64 = (1n << 64n) - 1n;
295
+ /**
296
+ * Read an identity from the form `Identity.export()` writes: the private key
297
+ * as canonical base64 of at most 32 bytes, a colon, and the offset as a
298
+ * decimal unsigned 64-bit integer. Anything else throws an
299
+ * InvalidIdentityError, with the underlying failure as its cause.
300
+ */
301
+ function parseIdentity(s) {
302
+ const colonIdx = s.lastIndexOf(":");
303
+ if (colonIdx < 0) throw new InvalidIdentityError();
304
+ const dBase64 = s.slice(0, colonIdx);
305
+ const offsetStr = s.slice(colonIdx + 1);
306
+ if (!/^\d+$/.test(offsetStr) || BigInt(offsetStr) > MAX_UINT64) throw new InvalidIdentityError("invalid identity offset");
307
+ const offset = BigInt(offsetStr);
308
+ const dBytes = Buffer.from(dBase64, "base64");
309
+ if (dBytes.length === 0 || dBytes.toString("base64") !== dBase64) throw new InvalidIdentityError("the private key is not canonical base64");
310
+ if (dBytes.length > P256_SCALAR_SIZE) throw new InvalidIdentityError("private key scalar too large");
311
+ const padded = Buffer.alloc(P256_SCALAR_SIZE);
312
+ dBytes.copy(padded, P256_SCALAR_SIZE - dBytes.length);
313
+ try {
314
+ const ecdh = createECDH("prime256v1");
315
+ ecdh.setPrivateKey(padded);
316
+ const pub = ecdh.getPublicKey(null, "uncompressed");
317
+ const x = Buffer.from(pub.slice(1, 33)).toString("base64url");
318
+ const y = Buffer.from(pub.slice(33, 65)).toString("base64url");
319
+ const jwk = {
320
+ kty: "EC",
321
+ crv: "P-256",
322
+ d: padded.toString("base64url"),
323
+ x,
324
+ y
325
+ };
326
+ return createIdentity(createPrivateKey({
327
+ key: jwk,
328
+ format: "jwk"
329
+ }), offset);
330
+ } catch (error) {
331
+ throw new InvalidIdentityError("invalid private key", { cause: error });
332
+ }
333
+ }
334
+ /**
335
+ * Create an identity at the given security level, at most 160. Each level
336
+ * doubles the expected work; `signal` bounds it, and the event loop keeps
337
+ * running meanwhile.
338
+ */
339
+ async function generateIdentity(targetLevel, options = {}) {
340
+ checkLevel(targetLevel);
341
+ return createIdentity(generateKey(), 0n).upgradeToLevel(targetLevel, options);
342
+ }
343
+ /**
344
+ * Create an identity at the given security level, at most 160, blocking until
345
+ * done. Fine for a low level; a high one can block the event loop for minutes,
346
+ * so prefer `generateIdentity()` there.
347
+ */
348
+ function generateIdentitySync(targetLevel) {
349
+ checkLevel(targetLevel);
350
+ const key = generateKey();
351
+ const prefix = createIdentity(key, 0n).publicKey();
352
+ let offset = 0n;
353
+ while (levelAt(prefix, offset) < targetLevel) offset++;
354
+ return createIdentity(key, offset);
355
+ }
356
+ function generateKey() {
357
+ return generateKeyPairSync("ec", { namedCurve: "prime256v1" }).privateKey;
358
+ }
359
+ /** Pick the UID hash from a `virtualserver_version` string, e.g. "6.0.0-beta13.1". */
360
+ function uidAlgorithmForVersion(version) {
361
+ const major = Number.parseInt(version, 10);
362
+ return Number.isNaN(major) || major < 6 ? "sha1" : "sha256";
363
+ }
364
+ /**
365
+ * The unique identifier of a public key in TeamSpeak's encoding, hashed with
366
+ * `algorithm`: SHA-1 for a TeamSpeak 3 server, SHA-256 for TeamSpeak 6.
367
+ */
368
+ function uidFromPublicKey(publicKey, algorithm) {
369
+ return asUID(createHash(algorithm).update(publicKey).digest("base64"));
370
+ }
371
+ function hash512(data) {
372
+ return createHash("sha512").update(data).digest();
373
+ }
374
+ /**
375
+ * Encode a P-256 public key as the TS3 canonical ASN.1 SEQUENCE and return
376
+ * its base64 representation.
377
+ *
378
+ * TS3 canonical format (v3+):
379
+ * SEQUENCE {
380
+ * BIT STRING { unused=7, value=0x00 } — DER: 03 02 07 00
381
+ * INTEGER 32
382
+ * INTEGER x
383
+ * INTEGER y
384
+ * }
385
+ */
386
+ function encodeTsPublicKey(x, y) {
387
+ const bitStringDer = encodeDerTlv(3, Buffer.from([7, 0]));
388
+ const integerSize = encodeDerTlv(2, encodePositiveInt(32));
389
+ const integerX = encodeDerTlv(2, encodePositiveInt(x));
390
+ const integerY = encodeDerTlv(2, encodePositiveInt(y));
391
+ return encodeDerTlv(48, Buffer.concat([
392
+ bitStringDer,
393
+ integerSize,
394
+ integerX,
395
+ integerY
396
+ ])).toString("base64");
397
+ }
398
+ /** Import a TS3 public key (canonical or legacy ASN.1 DER) and return the
399
+ * uncompressed 65-byte point [0x04 || x || y]. */
400
+ function importPublicKey(data) {
401
+ const point = parseTsPublicKeyDer(data);
402
+ const jwk = {
403
+ kty: "EC",
404
+ crv: "P-256",
405
+ x: Buffer.from(point.slice(1, 33)).toString("base64url"),
406
+ y: Buffer.from(point.slice(33, 65)).toString("base64url")
407
+ };
408
+ return createPublicKey({
409
+ key: jwk,
410
+ format: "jwk"
411
+ });
412
+ }
413
+ /**
414
+ * Parse both TS3 canonical and legacy ASN.1 formats and return the
415
+ * uncompressed 65-byte point [0x04 || x || y].
416
+ */
417
+ function parseTsPublicKeyDer(data) {
418
+ try {
419
+ return parseTsCanonicalPublicKey(data);
420
+ } catch {}
421
+ return parseTsLegacyPublicKey(data);
422
+ }
423
+ function parseTsCanonicalPublicKey(data) {
424
+ const seq = parseDerTlv(data, 48).value;
425
+ let offset = 0;
426
+ const bs = parseDerTlv(seq, 3, offset);
427
+ offset += bs.consumed;
428
+ const sizeInt = parseDerTlv(seq, 2, offset);
429
+ offset += sizeInt.consumed;
430
+ const xInt = parseDerTlv(seq, 2, offset);
431
+ offset += xInt.consumed;
432
+ const yInt = parseDerTlv(seq, 2, offset);
433
+ return buildUncompressedPoint(positiveIntToBytes(xInt.value, P256_SCALAR_SIZE), positiveIntToBytes(yInt.value, P256_SCALAR_SIZE));
434
+ }
435
+ function parseTsLegacyPublicKey(data) {
436
+ const seq = parseDerTlv(data, 48).value;
437
+ let offset = 0;
438
+ const xInt = parseDerTlv(seq, 2, offset);
439
+ offset += xInt.consumed;
440
+ const yInt = parseDerTlv(seq, 2, offset);
441
+ return buildUncompressedPoint(positiveIntToBytes(xInt.value, P256_SCALAR_SIZE), positiveIntToBytes(yInt.value, P256_SCALAR_SIZE));
442
+ }
443
+ function buildUncompressedPoint(x, y) {
444
+ if (x.length > P256_SCALAR_SIZE || y.length > P256_SCALAR_SIZE) throw new Error("invalid public key point encoding");
445
+ const point = new Uint8Array(P256_UNCOMPRESSED_KEY_SIZE);
446
+ point[0] = P256_POINT_PREFIX;
447
+ point.set(x, 33 - x.length);
448
+ point.set(y, 65 - y.length);
449
+ return point;
450
+ }
451
+ function encodeDerTlv(tag, value) {
452
+ const lenBytes = encodeDerLength(value.length);
453
+ return Buffer.concat([
454
+ Buffer.from([tag]),
455
+ lenBytes,
456
+ value
457
+ ]);
458
+ }
459
+ function encodeDerLength(len) {
460
+ if (len < 128) return Buffer.from([len]);
461
+ if (len < 256) return Buffer.from([129, len]);
462
+ return Buffer.from([
463
+ 130,
464
+ len >> 8 & 255,
465
+ len & 255
466
+ ]);
467
+ }
468
+ /** Encode a non-negative value as a DER INTEGER (with 0x00 prefix if high bit set). */
469
+ function encodePositiveInt(v) {
470
+ if (typeof v === "number") {
471
+ const bytes = [];
472
+ let n = v;
473
+ do {
474
+ bytes.unshift(n & 255);
475
+ n >>= 8;
476
+ } while (n > 0);
477
+ if ((bytes[0] & 128) !== 0) bytes.unshift(0);
478
+ return Buffer.from(bytes);
479
+ }
480
+ let start = 0;
481
+ while (start < v.length - 1 && v[start] === 0) start++;
482
+ const trimmed = v.slice(start);
483
+ if ((trimmed[0] & 128) !== 0) return Buffer.concat([Buffer.from([0]), trimmed]);
484
+ return Buffer.from(trimmed);
485
+ }
486
+ function parseDerTlv(data, expectedTag, offset = 0) {
487
+ if (data[offset] !== expectedTag) throw new Error(`expected DER tag 0x${expectedTag.toString(16)}, got 0x${(data[offset] ?? 0).toString(16)}`);
488
+ let pos = offset + 1;
489
+ let len;
490
+ const firstLenByte = data[pos++];
491
+ if (firstLenByte < 128) len = firstLenByte;
492
+ else if (firstLenByte === 129) len = data[pos++];
493
+ else if (firstLenByte === 130) {
494
+ len = (data[pos] << 8 | data[pos + 1]) >>> 0;
495
+ pos += 2;
496
+ } else throw new Error("unsupported DER length encoding");
497
+ return {
498
+ value: data.slice(pos, pos + len),
499
+ consumed: pos - offset + len
500
+ };
501
+ }
502
+ /** Convert a DER INTEGER value (may have leading 0x00) to fixed-length bytes. */
503
+ function positiveIntToBytes(int, size) {
504
+ let start = 0;
505
+ while (start < int.length - 1 && int[start] === 0) start++;
506
+ const stripped = int.slice(start);
507
+ if (stripped.length > size) throw new Error("integer too large");
508
+ if (stripped.length === size) return stripped;
509
+ const padded = new Uint8Array(size);
510
+ padded.set(stripped, size - stripped.length);
511
+ return padded;
512
+ }
513
+ function base64UrlToBytes$1(b64url, size) {
514
+ const buf = Buffer.from(b64url, "base64url");
515
+ if (buf.length === size) return buf;
516
+ const padded = Buffer.alloc(size);
517
+ buf.copy(padded, size - buf.length);
518
+ return padded;
519
+ }
520
+ function countLeadingZeros(data) {
521
+ let zeros = 0;
522
+ for (const b of data) if (b === 0) zeros += 8;
523
+ else for (let i = 0; i < 8; i++) if ((b & 1 << i) === 0) zeros++;
524
+ else return zeros;
525
+ return zeros;
526
+ }
527
+ //#endregion
528
+ //#region src/crypto/eax.ts
529
+ var EAX_TAG_SIZE = 8;
530
+ var EAX_BLOCK_SIZE = 16;
531
+ /**
532
+ * AES-EAX AEAD implementation for TS3 (64-bit tag, AES-128).
533
+ * Uses AES-CMAC for authentication and AES-CTR for encryption.
534
+ */
535
+ var EAX = class {
536
+ #key;
537
+ constructor(key) {
538
+ if (key.length !== 16) throw new Error("EAX requires a 16-byte key");
539
+ this.#key = key;
540
+ }
541
+ encrypt(nonce, header, plaintext) {
542
+ const nStar = this.#cmac(0, nonce);
543
+ const hStar = this.#cmac(1, header);
544
+ const ciphertext = aesCtr(this.#key, nStar, plaintext);
545
+ const cStar = this.#cmac(2, ciphertext);
546
+ const tag = new Uint8Array(EAX_TAG_SIZE);
547
+ for (let i = 0; i < EAX_TAG_SIZE; i++) tag[i] = nStar[i] ^ hStar[i] ^ cStar[i];
548
+ return [ciphertext, tag];
549
+ }
550
+ decrypt(nonce, header, ciphertext, tag) {
551
+ const nStar = this.#cmac(0, nonce);
552
+ const hStar = this.#cmac(1, header);
553
+ const cStar = this.#cmac(2, ciphertext);
554
+ const expected = new Uint8Array(EAX_TAG_SIZE);
555
+ for (let i = 0; i < EAX_TAG_SIZE; i++) expected[i] = nStar[i] ^ hStar[i] ^ cStar[i];
556
+ if (!timingSafeEqual(Buffer.from(expected), Buffer.from(tag.slice(0, EAX_TAG_SIZE)))) throw new EAXTagMismatchError();
557
+ return aesCtr(this.#key, nStar, ciphertext);
558
+ }
559
+ /**
560
+ * AES-CMAC per RFC 4493:
561
+ * input = [0…0 tag] ++ data (one block prefix)
562
+ */
563
+ #cmac(tagByte, data) {
564
+ const input = new Uint8Array(EAX_BLOCK_SIZE + data.length);
565
+ input[15] = tagByte;
566
+ input.set(data, EAX_BLOCK_SIZE);
567
+ return aesCmac(this.#key, input);
568
+ }
569
+ };
570
+ function aesCtr(key, iv, data) {
571
+ const cipher = createCipheriv("aes-128-ctr", Buffer.from(key), Buffer.from(iv));
572
+ return Buffer.concat([cipher.update(Buffer.from(data)), cipher.final()]);
573
+ }
574
+ function aesEcb(key, block) {
575
+ const cipher = createCipheriv("aes-128-ecb", Buffer.from(key), null);
576
+ cipher.setAutoPadding(false);
577
+ return Buffer.concat([cipher.update(Buffer.from(block)), cipher.final()]);
578
+ }
579
+ function xorBlock(a, b) {
580
+ const out = new Uint8Array(EAX_BLOCK_SIZE);
581
+ for (let i = 0; i < EAX_BLOCK_SIZE; i++) out[i] = a[i] ^ b[i];
582
+ return out;
583
+ }
584
+ function shiftLeft1(b) {
585
+ const out = new Uint8Array(EAX_BLOCK_SIZE);
586
+ let carry = 0;
587
+ for (let i = 15; i >= 0; i--) {
588
+ const shifted = (b[i] << 1 | carry) & 255;
589
+ carry = b[i] >> 7;
590
+ out[i] = shifted;
591
+ }
592
+ return out;
593
+ }
594
+ var RB = new Uint8Array(EAX_BLOCK_SIZE);
595
+ RB[15] = 135;
596
+ function generateSubkeys(key) {
597
+ const L = aesEcb(key, new Uint8Array(EAX_BLOCK_SIZE));
598
+ let K1 = shiftLeft1(L);
599
+ if ((L[0] & 128) !== 0) K1 = xorBlock(K1, RB);
600
+ let K2 = shiftLeft1(K1);
601
+ if ((K1[0] & 128) !== 0) K2 = xorBlock(K2, RB);
602
+ return [K1, K2];
603
+ }
604
+ function aesCmac(key, message) {
605
+ const [K1, K2] = generateSubkeys(key);
606
+ const n = Math.max(1, Math.ceil(message.length / EAX_BLOCK_SIZE));
607
+ const lastBlockComplete = message.length > 0 && message.length % EAX_BLOCK_SIZE === 0;
608
+ let X = new Uint8Array(EAX_BLOCK_SIZE);
609
+ for (let i = 0; i < n - 1; i++) {
610
+ const block = message.slice(i * EAX_BLOCK_SIZE, (i + 1) * EAX_BLOCK_SIZE);
611
+ X = Uint8Array.from(aesEcb(key, xorBlock(X, block)));
612
+ }
613
+ const lastBlock = new Uint8Array(EAX_BLOCK_SIZE);
614
+ const lastStart = (n - 1) * EAX_BLOCK_SIZE;
615
+ const lastSlice = message.slice(lastStart);
616
+ lastBlock.set(lastSlice);
617
+ let Mn;
618
+ if (lastBlockComplete) Mn = xorBlock(lastBlock, K1);
619
+ else {
620
+ if (lastSlice.length < EAX_BLOCK_SIZE) lastBlock[lastSlice.length] = 128;
621
+ Mn = xorBlock(lastBlock, K2);
622
+ }
623
+ return Uint8Array.from(aesEcb(key, xorBlock(X, Mn)));
624
+ }
625
+ //#endregion
626
+ //#region src/crypto/crypt.ts
627
+ var FAKE_SIGNATURE_SIZE = 8;
628
+ var IV_ALPHA_SIZE = 10;
629
+ var INIT1_PACKET_TYPE = 8;
630
+ var PACKET_TYPE_MASK = 15;
631
+ var INIT1_MAC = Buffer.from("TS3INIT1");
632
+ /** Precomputed dummy key/nonce matching the TS3 client pre-crypto placeholder. */
633
+ var DUMMY_KEY = Buffer.from("c:\\windows\\syste");
634
+ var DUMMY_NONCE = Buffer.from("m\\firewall32.cpl");
635
+ /**
636
+ * A connection's encryption state. The session material — the IV the keys
637
+ * derive from — lives in private fields, so it shows in no `util.inspect()`
638
+ * or `JSON.stringify()` of anything holding a Crypt.
639
+ */
640
+ var Crypt = class {
641
+ identity;
642
+ #ivStruct = /* @__PURE__ */ new Uint8Array(0);
643
+ #fakeSignature = new Uint8Array(FAKE_SIGNATURE_SIZE);
644
+ #alpha = /* @__PURE__ */ new Uint8Array(0);
645
+ #cryptoInitComplete = false;
646
+ #cachedKeys = /* @__PURE__ */ new Map();
647
+ constructor(identity) {
648
+ this.identity = identity;
649
+ }
650
+ /** Whether the session keys are set up; until then packets use the dummy key. */
651
+ get cryptoInitComplete() {
652
+ return this.#cryptoInitComplete;
653
+ }
654
+ /** The signature unencrypted packets carry: zeros until crypto init completes. */
655
+ get fakeSignature() {
656
+ return this.#fakeSignature;
657
+ }
658
+ /** The client's random contribution to the IV, sent in clientinitiv; empty before. */
659
+ get alpha() {
660
+ return this.#alpha;
661
+ }
662
+ /** Choose the client's random contribution to the IV, and return it. */
663
+ newAlpha() {
664
+ this.#alpha = new Uint8Array(randomBytes(IV_ALPHA_SIZE));
665
+ return this.#alpha;
666
+ }
667
+ solveRsaChallenge(data, offset, level) {
668
+ if (!Number.isInteger(level) || level < 0 || level > 1e5) throw new CryptoInitError(`RSA challenge level ${level} out of range`);
669
+ const xBytes = data.slice(offset, offset + 64);
670
+ const nBytes = data.slice(offset + 64, offset + 128);
671
+ let y = bytesToBigInt(xBytes);
672
+ const n = bytesToBigInt(nBytes);
673
+ if (n === 0n) throw new CryptoInitError("RSA challenge modulus is zero");
674
+ for (let i = 0; i < level; i++) y = y * y % n;
675
+ return bigIntToBytes(y, 64);
676
+ }
677
+ initCrypto(alpha, beta, omega) {
678
+ const alphaBytes = Buffer.from(alpha, "base64");
679
+ const betaBytes = Buffer.from(beta, "base64");
680
+ const omegaBytes = Buffer.from(omega, "base64");
681
+ let sharedSecret;
682
+ try {
683
+ sharedSecret = this.#getSharedSecret(importPublicKey(omegaBytes));
684
+ } catch (error) {
685
+ throw new CryptoInitError("the server's public key is invalid", { cause: error });
686
+ }
687
+ this.setSharedSecret(alphaBytes, betaBytes, sharedSecret);
688
+ }
689
+ setSharedSecret(alpha, beta, sharedKey) {
690
+ this.#ivStruct = new Uint8Array(IV_ALPHA_SIZE + beta.length);
691
+ for (let i = 0; i < IV_ALPHA_SIZE; i++) {
692
+ const a = sharedKey[i];
693
+ const b = alpha[i];
694
+ this.#ivStruct[i] = ((a !== void 0 ? a : 0) ^ (b !== void 0 ? b : 0)) & 255;
695
+ }
696
+ for (let i = 0; i < beta.length; i++) {
697
+ const a = sharedKey[IV_ALPHA_SIZE + i];
698
+ const b = beta[i];
699
+ this.#ivStruct[IV_ALPHA_SIZE + i] = ((a !== void 0 ? a : 0) ^ (b !== void 0 ? b : 0)) & 255;
700
+ }
701
+ this.#cachedKeys.clear();
702
+ const digest = createHash("sha1").update(this.#ivStruct).digest();
703
+ this.#fakeSignature = new Uint8Array(digest.buffer, digest.byteOffset, FAKE_SIGNATURE_SIZE);
704
+ this.#cryptoInitComplete = true;
705
+ }
706
+ getKeyNonce(fromServer, packetID, generationID, packetType, dummy) {
707
+ if (dummy) return [Uint8Array.from(DUMMY_KEY), Uint8Array.from(DUMMY_NONCE)];
708
+ if (!this.#cryptoInitComplete) throw new CryptoInitError("no session key before crypto init completes");
709
+ const cacheKey = makeCacheKey(fromServer, packetType, generationID);
710
+ let kn = this.#cachedKeys.get(cacheKey);
711
+ if (kn === void 0) {
712
+ const tmpToHash = new Uint8Array(6 + this.#ivStruct.length);
713
+ tmpToHash[0] = fromServer ? 48 : 49;
714
+ tmpToHash[1] = packetType & PACKET_TYPE_MASK;
715
+ new DataView(tmpToHash.buffer).setUint32(2, generationID, false);
716
+ tmpToHash.set(this.#ivStruct, 6);
717
+ const hash = createHash("sha256").update(tmpToHash).digest();
718
+ kn = {
719
+ key: Uint8Array.from(hash.slice(0, 16)),
720
+ nonce: Uint8Array.from(hash.slice(16, 32)),
721
+ gen: generationID
722
+ };
723
+ this.#cachedKeys.set(cacheKey, kn);
724
+ }
725
+ const key = Uint8Array.from(kn.key);
726
+ key[0] = (key[0] !== void 0 ? key[0] : 0) ^ packetID >> 8 & 255;
727
+ key[1] = (key[1] !== void 0 ? key[1] : 0) ^ packetID & 255;
728
+ return [key, kn.nonce];
729
+ }
730
+ encrypt(packetType, packetID, generationID, header, plaintext, dummy, unencrypted) {
731
+ if (packetType === INIT1_PACKET_TYPE) return [plaintext, INIT1_MAC];
732
+ if (unencrypted) return [plaintext, this.#fakeSignature];
733
+ const [key, nonce] = this.getKeyNonce(false, packetID, generationID, packetType, dummy);
734
+ return new EAX(key).encrypt(nonce, header, plaintext);
735
+ }
736
+ decrypt(packetType, packetID, generationID, header, ciphertext, tag, dummy, unencrypted) {
737
+ if (packetType === INIT1_PACKET_TYPE) return ciphertext;
738
+ if (unencrypted) {
739
+ const fsSub = tag.slice(0, FAKE_SIGNATURE_SIZE);
740
+ if (!Buffer.from(fsSub).equals(Buffer.from(this.#fakeSignature))) throw new FakeSignatureMismatchError();
741
+ return ciphertext;
742
+ }
743
+ const [key, nonce] = this.getKeyNonce(true, packetID, generationID, packetType, dummy);
744
+ return new EAX(key).decrypt(nonce, header, ciphertext, tag);
745
+ }
746
+ #getSharedSecret(serverPubKey) {
747
+ const privJwk = identityPrivateKey(this.identity).export({ format: "jwk" });
748
+ const pubJwk = serverPubKey.export({ format: "jwk" });
749
+ const ecdh = createECDH("prime256v1");
750
+ const dBytes = Buffer.from(privJwk.d, "base64url");
751
+ ecdh.setPrivateKey(dBytes);
752
+ const xBytes = base64UrlToBytes(pubJwk.x, 32);
753
+ const yBytes = base64UrlToBytes(pubJwk.y, 32);
754
+ const uncompressed = Buffer.alloc(65);
755
+ uncompressed[0] = 4;
756
+ xBytes.copy(uncompressed, 1);
757
+ yBytes.copy(uncompressed, 33);
758
+ const rawSecret = ecdh.computeSecret(uncompressed);
759
+ return createHash("sha1").update(rawSecret).digest();
760
+ }
761
+ };
762
+ function makeCacheKey(fromServer, packetType, generationID) {
763
+ let key = 0n;
764
+ if (fromServer) key |= 1n << 40n;
765
+ key |= BigInt(packetType & PACKET_TYPE_MASK) << 32n;
766
+ key |= BigInt(generationID);
767
+ return key;
768
+ }
769
+ function bytesToBigInt(bytes) {
770
+ let result = 0n;
771
+ for (const b of bytes) result = result << 8n | BigInt(b);
772
+ return result;
773
+ }
774
+ function bigIntToBytes(value, size) {
775
+ const result = new Uint8Array(size);
776
+ let v = value;
777
+ for (let i = size - 1; i >= 0; i--) {
778
+ result[i] = Number(v & 255n);
779
+ v >>= 8n;
780
+ }
781
+ return result;
782
+ }
783
+ function base64UrlToBytes(b64url, size) {
784
+ const buf = Buffer.from(b64url, "base64url");
785
+ if (buf.length === size) return buf;
786
+ const padded = Buffer.alloc(size);
787
+ buf.copy(padded, size - buf.length);
788
+ return padded;
789
+ }
790
+ //#endregion
791
+ //#region src/crypto/primitives.ts
792
+ var CURVE25519_KEY_SIZE = 32;
793
+ var CLAMP_MASK_LOW = 248;
794
+ var CLAMP_MASK_HIGH = 127;
795
+ var CLAMP_HIGH_BIT = 64;
796
+ var SHARED_SIGN_BIT = 128;
797
+ var PRIVATE_KEY_TOP_MASK = 127;
798
+ /** Clamp a Curve25519 scalar in-place (required before scalar multiplication). */
799
+ function clampScalar(key) {
800
+ if (key.length < CURVE25519_KEY_SIZE) return;
801
+ key[0] = (key[0] ?? 0) & CLAMP_MASK_LOW;
802
+ key[31] = (key[31] ?? 0) & CLAMP_MASK_HIGH | CLAMP_HIGH_BIT;
803
+ }
804
+ /**
805
+ * Generate an ephemeral Ed25519 key pair for the TS3 handshake.
806
+ * Returns [publicKey (32 bytes, RFC-8032 encoded), privateKey (32 bytes)].
807
+ */
808
+ function generateTemporaryKey() {
809
+ const privateKey = new Uint8Array(randomBytes(CURVE25519_KEY_SIZE));
810
+ clampScalar(privateKey);
811
+ const scalar = bytesToBigIntLE(privateKey) % ed25519.Point.CURVE().n;
812
+ return [ed25519.Point.BASE.multiply(scalar).toBytes(), Uint8Array.from(privateKey)];
813
+ }
814
+ /**
815
+ * Sign data with a P-256 private key (SHA-256 hash, ASN.1 DER encoded).
816
+ */
817
+ function sign(privateKey, data) {
818
+ const signer = createSign("SHA256");
819
+ signer.update(data);
820
+ return signer.sign(privateKey);
821
+ }
822
+ /**
823
+ * Verify a P-256 ECDSA signature (SHA-256 hash, ASN.1 DER encoded).
824
+ */
825
+ function verifySign(publicKey, data, sig) {
826
+ try {
827
+ const verifier = createVerify("SHA256");
828
+ verifier.update(data);
829
+ return verifier.verify(publicKey, Buffer.from(sig));
830
+ } catch {
831
+ return false;
832
+ }
833
+ }
834
+ /**
835
+ * TS3-specific shared secret derivation using Ed25519 point arithmetic.
836
+ * Mirrors NaCl's ge_scalarmult_vartime:
837
+ * 1. Negate the public point
838
+ * 2. Scalar-multiply by clamped private key bytes (NOT reduced mod n)
839
+ * 3. Flip sign bit of result
840
+ * 4. SHA-512 the resulting point bytes
841
+ *
842
+ * The scalar must NOT be reduced mod n: Ed25519 has cofactor 8, so the server's
843
+ * public key may have a small-order component, and P*s ≠ P*(s mod n) in that case.
844
+ * We use scalarMultFull to handle scalars >= n without losing the cofactor term.
845
+ */
846
+ function getSharedSecret2(publicKeyBytes, privateKeyBytes) {
847
+ if (publicKeyBytes.length !== CURVE25519_KEY_SIZE || privateKeyBytes.length !== CURVE25519_KEY_SIZE) throw new Error("invalid key length");
848
+ const privCopy = Uint8Array.from(privateKeyBytes);
849
+ privCopy[31] = (privCopy[31] ?? 0) & PRIVATE_KEY_TOP_MASK;
850
+ const rawScalar = bytesToBigIntLE(privCopy);
851
+ const sharedBytes = scalarMultFull(ed25519.Point.fromBytes(publicKeyBytes).negate(), rawScalar).toBytes();
852
+ sharedBytes[31] = (sharedBytes[31] ?? 0) ^ SHARED_SIGN_BIT;
853
+ return createHash("sha512").update(sharedBytes).digest();
854
+ }
855
+ /**
856
+ * Multiply an Ed25519 point by a scalar that may be >= curve order n.
857
+ * Decomposes as: P*s = P*(s mod n) + (P*n)*q where q = floor(s/n).
858
+ * This preserves the small-order component (cofactor 8) that would be
859
+ * lost by reducing mod n first.
860
+ *
861
+ * For a clamped 255-bit scalar, q is at most 7 (since maxScalar / n < 8),
862
+ * so the second term is computed with at most one extra scalar multiplication.
863
+ */
864
+ function scalarMultFull(point, scalar) {
865
+ const n = ed25519.Point.CURVE().n;
866
+ if (scalar < n) return scalar === 0n ? ed25519.Point.ZERO : point.multiply(scalar);
867
+ const remainder = scalar % n;
868
+ const quotient = scalar / n;
869
+ const mainPart = remainder === 0n ? ed25519.Point.ZERO : point.multiply(remainder);
870
+ const cofactorPart = point.multiply(n - 1n).add(point).multiply(quotient);
871
+ return mainPart.add(cofactorPart);
872
+ }
873
+ /** Interpret a little-endian byte array as an unsigned BigInt. */
874
+ function bytesToBigIntLE(bytes) {
875
+ let result = 0n;
876
+ for (let i = bytes.length - 1; i >= 0; i--) result = result << 8n | BigInt(bytes[i]);
877
+ return result;
878
+ }
879
+ //#endregion
880
+ //#region src/address.ts
881
+ /** Parse a TeamSpeak address without confusing an IPv6 colon for a port separator. */
882
+ function splitHostPort(address, defaultPort = "9987") {
883
+ const input = address.trim();
884
+ if (input.startsWith("[")) {
885
+ const closingBracket = input.indexOf("]");
886
+ if (closingBracket > 0) {
887
+ const host = input.slice(1, closingBracket);
888
+ const suffix = input.slice(closingBracket + 1);
889
+ if (suffix.startsWith(":") && /^\d+$/.test(suffix.slice(1))) return {
890
+ host,
891
+ port: suffix.slice(1)
892
+ };
893
+ if (!suffix) return {
894
+ host,
895
+ port: defaultPort
896
+ };
897
+ }
898
+ }
899
+ if (isIP(input) === 6) return {
900
+ host: input,
901
+ port: defaultPort
902
+ };
903
+ const lastColon = input.lastIndexOf(":");
904
+ if (lastColon < 0) return {
905
+ host: input,
906
+ port: defaultPort
907
+ };
908
+ const candidatePort = input.slice(lastColon + 1);
909
+ if (/^\d+$/.test(candidatePort) && input.indexOf(":") === lastColon) return {
910
+ host: input.slice(0, lastColon),
911
+ port: candidatePort
912
+ };
913
+ return {
914
+ host: input,
915
+ port: defaultPort
916
+ };
917
+ }
918
+ /** Characters no host name or IP address contains, whitespace among them. */
919
+ var NOT_IN_A_HOST = /[\s\p{Cc}/?#@[\]\\]/u;
920
+ /**
921
+ * Parse an address to connect to, and check it can be one: a host, which
922
+ * contains nothing a host name or IP address cannot, and a port from 1 to
923
+ * 65535. Throws an InvalidAddressError otherwise.
924
+ */
925
+ function parseConnectAddress(address) {
926
+ const { host, port } = splitHostPort(address);
927
+ if (host === "") throw new InvalidAddressError(address, "no host");
928
+ if (NOT_IN_A_HOST.test(host) || host.includes(":") && isIP(host) !== 6) throw new InvalidAddressError(address, "not a host name, IP address or host:port");
929
+ const portNumber = Number(port);
930
+ if (!Number.isInteger(portNumber) || portNumber < 1 || portNumber > 65535) throw new InvalidAddressError(address, `port ${port} is not from 1 to 65535`);
931
+ return {
932
+ host,
933
+ port,
934
+ portNumber
935
+ };
936
+ }
937
+ function joinHostPort(host, port) {
938
+ const normalizedHost = stripBrackets(host);
939
+ return isIP(normalizedHost) === 6 || normalizedHost.includes(":") ? `[${normalizedHost}]:${port}` : `${normalizedHost}:${port}`;
940
+ }
941
+ function isIpAddress(host) {
942
+ return isIP(stripBrackets(host)) !== 0;
943
+ }
944
+ function stripBrackets(host) {
945
+ return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
946
+ }
947
+ //#endregion
948
+ //#region src/discovery/resolver.ts
949
+ var TS_DNS_DEFAULT_PORT = 41144;
950
+ var NICKNAME_LOOKUP_URL = "https://named.myteamspeak.com/lookup";
951
+ var CACHE_TTL_MS = 6e5;
952
+ /**
953
+ * How long resolution may take in all; past it, the address is used as given.
954
+ * A Client holds any resolver, its own or a custom one, to the same budget.
955
+ *
956
+ * @internal
957
+ */
958
+ var RESOLVE_BUDGET_MS = 1e4;
959
+ /** How long one TSDNS server may take to answer. */
960
+ var TSDNS_TIMEOUT_MS = 3e3;
961
+ var Resolver = class {
962
+ #cache = /* @__PURE__ */ new Map();
963
+ #resolveNicknames;
964
+ constructor(options = {}) {
965
+ this.#resolveNicknames = options.resolveNicknames ?? false;
966
+ }
967
+ /**
968
+ * Resolve as the TeamSpeak application does: a server nickname (only with
969
+ * `resolveNicknames`), then SRV, then TSDNS, then the address as given. The
970
+ * whole resolution is bounded: past the budget, or when a step fails, the
971
+ * address as given is the answer.
972
+ */
973
+ async resolve(inputAddr, options = {}) {
974
+ if (!inputAddr) throw new Error("empty address");
975
+ const budget = AbortSignal.timeout(RESOLVE_BUDGET_MS);
976
+ const signal = options.signal ? AbortSignal.any([options.signal, budget]) : budget;
977
+ try {
978
+ return await this.#resolve(inputAddr, signal, true);
979
+ } catch (error) {
980
+ options.signal?.throwIfAborted();
981
+ if (!budget.aborted) throw error;
982
+ const { host, port } = splitHostPort(inputAddr);
983
+ return [{
984
+ addr: joinHostPort(host, port),
985
+ source: "Direct"
986
+ }];
987
+ }
988
+ }
989
+ async #resolve(inputAddr, signal, nicknames) {
990
+ const cached = this.#getValidCache(inputAddr);
991
+ if (cached) return cached;
992
+ const { host, port } = splitHostPort(inputAddr);
993
+ if (isIpAddress(host)) return [{
994
+ addr: joinHostPort(host, port),
995
+ source: "Direct"
996
+ }];
997
+ if (nicknames && this.#resolveNicknames && !host.includes(".") && host !== "localhost") {
998
+ const nickAddr = await resolveNickname(host, signal);
999
+ signal.throwIfAborted();
1000
+ if (nickAddr) return this.#resolve(nickAddr, signal, false);
1001
+ }
1002
+ const srvResults = await this.#resolveSRV(host, signal);
1003
+ if (srvResults) return this.#setCache(inputAddr, srvResults);
1004
+ const domainList = getDomainList(host);
1005
+ const tsdnsSrv = await this.#resolveTSDNSSRV(domainList, host, signal);
1006
+ if (tsdnsSrv) return this.#setCache(inputAddr, [{
1007
+ addr: tsdnsSrv,
1008
+ source: "TSDNS-SRV"
1009
+ }]);
1010
+ const tsdnsDirect = await this.#resolveTSDNSDirect(domainList, host, signal);
1011
+ if (tsdnsDirect) return this.#setCache(inputAddr, [{
1012
+ addr: tsdnsDirect,
1013
+ source: "TSDNS-Direct"
1014
+ }]);
1015
+ signal.throwIfAborted();
1016
+ const fallback = [{
1017
+ addr: joinHostPort(host, port),
1018
+ source: "Direct"
1019
+ }];
1020
+ return this.#setCache(inputAddr, fallback);
1021
+ }
1022
+ #getValidCache(inputAddr) {
1023
+ const cached = this.#cache.get(inputAddr);
1024
+ if (!cached || cached.length === 0) return null;
1025
+ const first = cached[0];
1026
+ if (first.expiry !== void 0 && Date.now() > first.expiry.getTime()) return null;
1027
+ return cached;
1028
+ }
1029
+ #setCache(key, results) {
1030
+ const expiry = new Date(Date.now() + CACHE_TTL_MS);
1031
+ const withExpiry = results.map((r) => ({
1032
+ ...r,
1033
+ expiry
1034
+ }));
1035
+ this.#cache.set(key, withExpiry);
1036
+ return withExpiry;
1037
+ }
1038
+ async #resolveSRV(host, signal) {
1039
+ try {
1040
+ const srvs = await abortable(resolveSrv(`_ts3._udp.${host}`), signal);
1041
+ if (!srvs || srvs.length === 0) return null;
1042
+ return srvs.map((srv) => ({
1043
+ addr: joinHostPort(srv.name.replace(/\.$/, ""), String(srv.port)),
1044
+ source: "SRV"
1045
+ }));
1046
+ } catch {
1047
+ return null;
1048
+ }
1049
+ }
1050
+ async #resolveTSDNSSRV(domains, queryHost, signal) {
1051
+ for (const domain of domains) try {
1052
+ const srvs = await abortable(resolveSrv(`_tsdns._tcp.${domain}`), signal);
1053
+ if (!srvs || srvs.length === 0) continue;
1054
+ for (const srv of srvs) {
1055
+ const result = await queryTSDNS(joinHostPort(srv.name.replace(/\.$/, ""), String(srv.port)), queryHost, signal);
1056
+ if (result) return result;
1057
+ }
1058
+ } catch {
1059
+ continue;
1060
+ }
1061
+ return null;
1062
+ }
1063
+ async #resolveTSDNSDirect(domains, queryHost, signal) {
1064
+ return (await Promise.all(domains.map((domain) => queryTSDNS(joinHostPort(domain, String(TS_DNS_DEFAULT_PORT)), queryHost, signal)))).find((answer) => answer !== null) ?? null;
1065
+ }
1066
+ };
1067
+ /** @internal Ask the nickname service for `nickname`'s address. */
1068
+ async function resolveNickname(nickname, signal) {
1069
+ try {
1070
+ const url = new URL(NICKNAME_LOOKUP_URL);
1071
+ url.searchParams.set("name", nickname);
1072
+ const init = signal ? { signal } : {};
1073
+ const resp = await fetch(url.toString(), init);
1074
+ if (!resp.ok) return null;
1075
+ return (await readLimited(resp, 8192)).split("\n")[0]?.trim() || null;
1076
+ } catch {
1077
+ return null;
1078
+ }
1079
+ }
1080
+ /** A response body's first `limit` bytes, as text; the rest is not read. */
1081
+ async function readLimited(resp, limit) {
1082
+ if (resp.body === null) return "";
1083
+ const reader = resp.body.getReader();
1084
+ const chunks = [];
1085
+ let size = 0;
1086
+ try {
1087
+ while (size < limit) {
1088
+ const { done, value } = await reader.read();
1089
+ if (done) break;
1090
+ const part = value.subarray(0, limit - size);
1091
+ chunks.push(part);
1092
+ size += part.length;
1093
+ }
1094
+ } finally {
1095
+ reader.cancel().catch(() => {});
1096
+ }
1097
+ return Buffer.concat(chunks).toString("utf8");
1098
+ }
1099
+ /** @internal Ask the TSDNS server at `tsdnsAddr` for `queryHost`'s address. */
1100
+ function queryTSDNS(tsdnsAddr, queryHost, signal) {
1101
+ const [host, portStr] = splitHostPortParts(tsdnsAddr);
1102
+ const port = parseInt(portStr, 10);
1103
+ return new Promise((resolve) => {
1104
+ if (signal?.aborted) return resolve(null);
1105
+ const finish = (answer) => {
1106
+ clearTimeout(timeout);
1107
+ signal?.removeEventListener("abort", onAbort);
1108
+ socket.destroy();
1109
+ resolve(answer);
1110
+ };
1111
+ const onAbort = () => finish(null);
1112
+ const timeout = setTimeout(() => finish(null), TSDNS_TIMEOUT_MS);
1113
+ const socket = createConnection({
1114
+ host,
1115
+ port,
1116
+ timeout: 2e3
1117
+ }, () => {
1118
+ socket.write(`${queryHost}\n`);
1119
+ });
1120
+ let buf = "";
1121
+ socket.on("data", (chunk) => {
1122
+ buf += chunk.toString("latin1");
1123
+ const idx = buf.indexOf("\n");
1124
+ if (idx >= 0 && idx < 8192) {
1125
+ const line = Buffer.from(buf.slice(0, idx), "latin1").toString("utf8").trim();
1126
+ finish(!line || line === "404" || line === "errors" ? null : line);
1127
+ } else if (buf.length >= 8192) finish(null);
1128
+ });
1129
+ socket.on("error", () => finish(null));
1130
+ socket.on("timeout", () => finish(null));
1131
+ signal?.addEventListener("abort", onAbort, { once: true });
1132
+ });
1133
+ }
1134
+ function splitHostPortParts(addr) {
1135
+ const { host, port } = splitHostPort(addr);
1136
+ return [host, port];
1137
+ }
1138
+ function getDomainList(host) {
1139
+ const parts = host.split(".");
1140
+ const list = [];
1141
+ for (let i = 0; i < parts.length - 1; i++) list.push(parts.slice(i).join("."));
1142
+ return list.slice(0, 3);
1143
+ }
1144
+ //#endregion
1145
+ //#region src/transport/packet.ts
1146
+ var PacketType = /* @__PURE__ */ function(PacketType) {
1147
+ PacketType[PacketType["Voice"] = 0] = "Voice";
1148
+ PacketType[PacketType["VoiceWhisper"] = 1] = "VoiceWhisper";
1149
+ PacketType[PacketType["Command"] = 2] = "Command";
1150
+ PacketType[PacketType["CommandLow"] = 3] = "CommandLow";
1151
+ PacketType[PacketType["Ping"] = 4] = "Ping";
1152
+ PacketType[PacketType["Pong"] = 5] = "Pong";
1153
+ PacketType[PacketType["Ack"] = 6] = "Ack";
1154
+ PacketType[PacketType["AckLow"] = 7] = "AckLow";
1155
+ PacketType[PacketType["Init1"] = 8] = "Init1";
1156
+ return PacketType;
1157
+ }({});
1158
+ var PacketFlags = /* @__PURE__ */ function(PacketFlags) {
1159
+ PacketFlags[PacketFlags["Fragmented"] = 16] = "Fragmented";
1160
+ PacketFlags[PacketFlags["NewProtocol"] = 32] = "NewProtocol";
1161
+ PacketFlags[PacketFlags["Compressed"] = 64] = "Compressed";
1162
+ PacketFlags[PacketFlags["Unencrypted"] = 128] = "Unencrypted";
1163
+ return PacketFlags;
1164
+ }({});
1165
+ function packetType(p) {
1166
+ return p.typeFlagged & 15;
1167
+ }
1168
+ function packetFlags(p) {
1169
+ return p.typeFlagged & 240;
1170
+ }
1171
+ /** Build the 5-byte client-to-server header: [packetID(2), clientID(2), typeFlagged(1)]. */
1172
+ function buildC2SHeader(p) {
1173
+ const header = /* @__PURE__ */ new Uint8Array(5);
1174
+ const view = new DataView(header.buffer);
1175
+ view.setUint16(0, p.id, false);
1176
+ view.setUint16(2, p.clientID, false);
1177
+ header[4] = p.typeFlagged;
1178
+ return header;
1179
+ }
1180
+ /** Parse a 3-byte server-to-client header. */
1181
+ function parseS2CHeader(raw) {
1182
+ return {
1183
+ id: new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint16(0, false),
1184
+ typeFlagged: raw[2]
1185
+ };
1186
+ }
1187
+ //#endregion
1188
+ //#region src/transport/generation-window.ts
1189
+ var GenerationWindow = class {
1190
+ #mappedBaseOffset = 0;
1191
+ #generation = 0;
1192
+ #mod;
1193
+ #receiveWindow;
1194
+ constructor(mod, windowSize) {
1195
+ this.#mod = mod;
1196
+ this.#receiveWindow = windowSize;
1197
+ }
1198
+ get generation() {
1199
+ return this.#generation;
1200
+ }
1201
+ advance(amount) {
1202
+ if (amount <= 0) return;
1203
+ const newBaseOffset = this.#mappedBaseOffset + amount;
1204
+ const genStep = Math.floor(newBaseOffset / this.#mod);
1205
+ if (genStep > 0) this.#generation = Math.min(this.#generation + genStep, 4294967295);
1206
+ this.#mappedBaseOffset = newBaseOffset % this.#mod;
1207
+ }
1208
+ advanceToExcluded(mappedValue) {
1209
+ let moveDist = mappedValue - this.#mappedBaseOffset;
1210
+ if (moveDist < 0) moveDist += this.#mod;
1211
+ this.advance(moveDist + 1);
1212
+ }
1213
+ syncTo(mappedValue) {
1214
+ let moveDist = mappedValue - this.#mappedBaseOffset;
1215
+ if (moveDist < 0) moveDist += this.#mod;
1216
+ this.advance(moveDist);
1217
+ }
1218
+ isInWindow(mappedValue) {
1219
+ const maxOffset = this.#mappedBaseOffset + this.#receiveWindow;
1220
+ if (maxOffset < this.#mod) return mappedValue >= this.#mappedBaseOffset && mappedValue < maxOffset;
1221
+ return mappedValue >= this.#mappedBaseOffset || mappedValue < maxOffset - this.#mod;
1222
+ }
1223
+ mappedToIndex(mappedValue) {
1224
+ if (this.#isNextGen(mappedValue)) return mappedValue + this.#mod - this.#mappedBaseOffset;
1225
+ return mappedValue - this.#mappedBaseOffset;
1226
+ }
1227
+ isOldPacket(mappedValue) {
1228
+ return this.mappedToIndex(mappedValue) < 0;
1229
+ }
1230
+ isFuturePacket(mappedValue) {
1231
+ return this.mappedToIndex(mappedValue) >= this.#receiveWindow;
1232
+ }
1233
+ #isNextGen(mappedValue) {
1234
+ return this.#mappedBaseOffset > this.#mod - this.#receiveWindow && mappedValue < this.#mappedBaseOffset + this.#receiveWindow - this.#mod;
1235
+ }
1236
+ getGeneration(mappedValue) {
1237
+ if (this.#isNextGen(mappedValue)) return this.#generation + 1;
1238
+ return this.#generation;
1239
+ }
1240
+ reset() {
1241
+ this.#mappedBaseOffset = 0;
1242
+ this.#generation = 0;
1243
+ }
1244
+ };
1245
+ //#endregion
1246
+ //#region src/transport/quicklz.ts
1247
+ var TABLE_SIZE = 4096;
1248
+ /**
1249
+ * The largest decompressed command accepted. A command is reassembled from at
1250
+ * most a receive window of fragments, and the largest real answers (a full
1251
+ * permission list, the welcome channel list of a big server) are a few
1252
+ * hundred kilobytes; a header declaring more is refused before anything is
1253
+ * allocated.
1254
+ */
1255
+ var MAX_DECOMPRESSED_SIZE = 1 << 20;
1256
+ /**
1257
+ * How much larger than its compressed bytes a level-1 block can be. The most
1258
+ * a 3-byte back-reference yields is 255 bytes, and every 32 entries also cost
1259
+ * a 4-byte control word, which bounds the ratio at about 82; a block declaring
1260
+ * more cannot be honest, and is refused before its output is allocated.
1261
+ */
1262
+ var MAX_EXPANSION = 128;
1263
+ /** A QuickLZ block that cannot be decompressed: truncated, oversized or inconsistent. */
1264
+ var QuickLZError = class extends Error {
1265
+ constructor(message) {
1266
+ super(`QuickLZ: ${message}`);
1267
+ this.name = "QuickLZError";
1268
+ }
1269
+ };
1270
+ /**
1271
+ * QuickLZ level 1 decompression, as the server compresses large commands.
1272
+ *
1273
+ * The input comes from the network, so every read and every back-reference is
1274
+ * checked, and malformed input throws a QuickLZError rather than reading out
1275
+ * of range or producing a partly filled buffer.
1276
+ */
1277
+ var Qlz = class {
1278
+ #hashtable = new Int32Array(TABLE_SIZE);
1279
+ decompress(data) {
1280
+ const { headerLen, decompressedSize, flags } = parseQlzHeader(data);
1281
+ if ((flags & 1) === 0) {
1282
+ if (headerLen + decompressedSize > data.length) throw new QuickLZError("stored block is shorter than its declared size");
1283
+ return data.slice(headerLen, headerLen + decompressedSize);
1284
+ }
1285
+ if (decompressedSize > (data.length - headerLen) * MAX_EXPANSION) throw new QuickLZError(`declared size ${decompressedSize} is more than ${data.length - headerLen} bytes can expand to`);
1286
+ const dest = new Uint8Array(decompressedSize);
1287
+ this.#hashtable.fill(0);
1288
+ const state = {
1289
+ control: 1,
1290
+ sourcePos: headerLen,
1291
+ destPos: 0,
1292
+ nextHashed: 0
1293
+ };
1294
+ while (state.destPos < decompressedSize) {
1295
+ this.#ensureControl(data, state);
1296
+ if ((state.control & 1) !== 0) this.#processReference(data, dest, state);
1297
+ else if (this.#processLiteral(data, dest, decompressedSize, state)) break;
1298
+ }
1299
+ if (state.destPos !== decompressedSize) throw new QuickLZError("block ends early");
1300
+ return dest;
1301
+ }
1302
+ #ensureControl(data, st) {
1303
+ if (st.control !== 1) return;
1304
+ if (st.sourcePos + 4 > data.length) throw new QuickLZError("block ends early");
1305
+ st.control = (data[st.sourcePos] | data[st.sourcePos + 1] << 8 | data[st.sourcePos + 2] << 16 | data[st.sourcePos + 3] << 24) >>> 0;
1306
+ st.sourcePos += 4;
1307
+ }
1308
+ #processReference(data, dest, st) {
1309
+ st.control = st.control >>> 1 >>> 0;
1310
+ if (st.sourcePos + 2 > data.length) throw new QuickLZError("block ends early");
1311
+ const b1 = data[st.sourcePos];
1312
+ const b2 = data[st.sourcePos + 1];
1313
+ st.sourcePos += 2;
1314
+ const hash = b1 >> 4 | b2 << 4;
1315
+ let matchlen = b1 & 15;
1316
+ if (matchlen !== 0) matchlen += 2;
1317
+ else {
1318
+ if (st.sourcePos >= data.length) throw new QuickLZError("block ends early");
1319
+ matchlen = data[st.sourcePos];
1320
+ st.sourcePos++;
1321
+ }
1322
+ const offset = this.#hashtable[hash];
1323
+ if (offset >= st.destPos) throw new QuickLZError("back-reference points ahead of the output");
1324
+ if (st.destPos + matchlen > dest.length) throw new QuickLZError("back-reference overruns the declared size");
1325
+ for (let i = 0; i < matchlen; i++) {
1326
+ dest[st.destPos] = dest[offset + i];
1327
+ st.destPos++;
1328
+ }
1329
+ const end = st.destPos + 1 - matchlen;
1330
+ this.#updateHashtable(dest, st, end);
1331
+ st.nextHashed = st.destPos;
1332
+ }
1333
+ /** Copy literals; returns true once the tail of the block has been copied. */
1334
+ #processLiteral(data, dest, decompressedSize, st) {
1335
+ const threshold = Math.max(decompressedSize, 10) - 10;
1336
+ if (st.destPos >= threshold) {
1337
+ while (st.destPos < decompressedSize) {
1338
+ if (st.control === 1) {
1339
+ if (st.sourcePos + 4 > data.length) throw new QuickLZError("block ends early");
1340
+ st.control = (data[st.sourcePos] | data[st.sourcePos + 1] << 8 | data[st.sourcePos + 2] << 16 | data[st.sourcePos + 3] << 24) >>> 0;
1341
+ st.sourcePos += 4;
1342
+ }
1343
+ if (st.sourcePos >= data.length) throw new QuickLZError("block ends early");
1344
+ dest[st.destPos++] = data[st.sourcePos++];
1345
+ st.control = st.control >>> 1 >>> 0;
1346
+ }
1347
+ return true;
1348
+ }
1349
+ if (st.sourcePos >= data.length) throw new QuickLZError("block ends early");
1350
+ dest[st.destPos++] = data[st.sourcePos++];
1351
+ st.control = st.control >>> 1 >>> 0;
1352
+ const end = Math.max(st.destPos - 2, 0);
1353
+ this.#updateHashtable(dest, st, end);
1354
+ if (st.nextHashed < end) st.nextHashed = end;
1355
+ return false;
1356
+ }
1357
+ #updateHashtable(dest, st, end) {
1358
+ while (st.nextHashed < end) {
1359
+ if (st.nextHashed + 3 > dest.length) break;
1360
+ const v = (dest[st.nextHashed] | dest[st.nextHashed + 1] << 8 | dest[st.nextHashed + 2] << 16) >>> 0;
1361
+ const hash = (v >> 12 ^ v) & 4095;
1362
+ this.#hashtable[hash] = st.nextHashed;
1363
+ st.nextHashed++;
1364
+ }
1365
+ }
1366
+ };
1367
+ function parseQlzHeader(data) {
1368
+ if (data.length < 3) throw new QuickLZError("data too short");
1369
+ const flags = data[0];
1370
+ if ((flags >> 2 & 3) !== 1) throw new QuickLZError("only level 1 is supported");
1371
+ const long = (flags & 2) !== 0;
1372
+ const headerLen = long ? 9 : 3;
1373
+ if (data.length < headerLen) throw new QuickLZError("data too short for header");
1374
+ const decompressedSize = long ? (data[5] | data[6] << 8 | data[7] << 16 | data[8] << 24) >>> 0 : data[2];
1375
+ if (decompressedSize > 1048576) throw new QuickLZError(`declared size ${decompressedSize} exceeds ${MAX_DECOMPRESSED_SIZE}`);
1376
+ return {
1377
+ headerLen,
1378
+ decompressedSize,
1379
+ flags
1380
+ };
1381
+ }
1382
+ //#endregion
1383
+ //#region src/handshake/crypt-handshake.ts
1384
+ var INIT_VERSION = 1566914096;
1385
+ var INIT_STEP_LEN = 21;
1386
+ /** The RSA challenge each connection attempt, by its Crypt, has answered. */
1387
+ var answeredChallenges = /* @__PURE__ */ new WeakMap();
1388
+ /**
1389
+ * Handle the TS3INIT1 handshake steps.
1390
+ * Returns the response bytes to send, or null if nothing should be sent.
1391
+ * Throws a CryptoInitError for a step that cannot be answered.
1392
+ */
1393
+ function processInit1(crypt, data) {
1394
+ if (data === null || data.length >= 1 && data[0] === 127) return buildInit1StartPacket();
1395
+ if (data.length === 0) throw new CryptoInitError("empty Init1 packet");
1396
+ switch (data[0]) {
1397
+ case 0: return buildInit1Step1Packet(data);
1398
+ case 1: return buildInit1Step2Packet(data);
1399
+ case 2: return buildInit1Step3Packet(data);
1400
+ case 3: return buildInit1Step4Packet(crypt, data);
1401
+ default: return null;
1402
+ }
1403
+ }
1404
+ function buildInit1StartPacket() {
1405
+ const sendData = /* @__PURE__ */ new Uint8Array(21);
1406
+ const view = new DataView(sendData.buffer);
1407
+ view.setUint32(0, INIT_VERSION, false);
1408
+ sendData[4] = 0;
1409
+ const nowSec = Math.floor(Date.now() / 1e3);
1410
+ const clampedNow = Math.max(0, Math.min(nowSec, 4294967295));
1411
+ view.setUint32(5, clampedNow, false);
1412
+ const rng = randomBytes(4);
1413
+ sendData.set(rng, 9);
1414
+ return sendData;
1415
+ }
1416
+ function buildInit1Step1Packet(data) {
1417
+ if (data.length !== INIT_STEP_LEN) return null;
1418
+ const sendData = /* @__PURE__ */ new Uint8Array(21);
1419
+ sendData[0] = 1;
1420
+ const tsRand = data[9] | data[10] << 8 | data[11] << 16 | data[12] << 24;
1421
+ new DataView(sendData.buffer).setUint32(17, tsRand >>> 0, false);
1422
+ return sendData;
1423
+ }
1424
+ function buildInit1Step2Packet(data) {
1425
+ if (data.length !== INIT_STEP_LEN) return null;
1426
+ const sendData = /* @__PURE__ */ new Uint8Array(25);
1427
+ new DataView(sendData.buffer).setUint32(0, INIT_VERSION, false);
1428
+ sendData[4] = 2;
1429
+ sendData.set(data.slice(1, 21), 5);
1430
+ return sendData;
1431
+ }
1432
+ function buildInit1Step3Packet(data) {
1433
+ if (data.length !== 25) return null;
1434
+ const sendData = /* @__PURE__ */ new Uint8Array(233);
1435
+ sendData[0] = 3;
1436
+ sendData[64] = 1;
1437
+ sendData[128] = 1;
1438
+ new DataView(sendData.buffer).setUint32(129, 1, false);
1439
+ return sendData;
1440
+ }
1441
+ function buildInit1Step4Packet(crypt, data) {
1442
+ if (data.length !== 233) return null;
1443
+ const answered = answeredChallenges.get(crypt);
1444
+ if (answered !== void 0) return Buffer.from(answered.challenge).equals(Buffer.from(data)) ? answered.answer : null;
1445
+ const level = new DataView(data.buffer, data.byteOffset).getUint32(129, false);
1446
+ const y = crypt.solveRsaChallenge(data, 1, level);
1447
+ const alphaB64 = Buffer.from(crypt.newAlpha()).toString("base64");
1448
+ const omegaB64 = crypt.identity.publicKey();
1449
+ const cmd = buildCommandOrdered("clientinitiv", [
1450
+ ["alpha", alphaB64],
1451
+ ["omega", omegaB64],
1452
+ ["ot", "1"],
1453
+ ["ip", ""]
1454
+ ]);
1455
+ const cmdBytes = Buffer.from(cmd);
1456
+ const sendData = new Uint8Array(301 + cmdBytes.length);
1457
+ new DataView(sendData.buffer).setUint32(0, INIT_VERSION, false);
1458
+ sendData[4] = 4;
1459
+ sendData.set(data.slice(1, 233), 5);
1460
+ sendData.set(y.slice(0, 64), 237);
1461
+ sendData.set(cmdBytes, 301);
1462
+ answeredChallenges.set(crypt, {
1463
+ challenge: Uint8Array.from(data),
1464
+ answer: sendData
1465
+ });
1466
+ return sendData;
1467
+ }
1468
+ //#endregion
1469
+ //#region src/transport/handler.ts
1470
+ var RECEIVE_PACKET_WINDOW_SIZE = 1024;
1471
+ var PING_INTERVAL_MS = 5e3;
1472
+ var PACKET_TIMEOUT_MS = 6e4;
1473
+ var MAX_RETRY_INTERVAL_MS = 1e3;
1474
+ var TAG_SIZE = 8;
1475
+ var VOICE_HEADER_SIZE = 3;
1476
+ var WHISPER_HEADER_SIZE = 5;
1477
+ var RESEND_BASE_INTERVAL_MS = 500;
1478
+ var RESEND_LOOP_INTERVAL_MS = 100;
1479
+ /**
1480
+ * The largest command reassembled from fragments. The receive window already
1481
+ * bounds how many fragments one command can span; this bounds their total
1482
+ * size, which a datagram's own size limit would otherwise let reach tens of
1483
+ * megabytes.
1484
+ */
1485
+ var MAX_REASSEMBLED_SIZE = 1 << 20;
1486
+ /**
1487
+ * The packet types the server may send unencrypted: voice, which TeamSpeak
1488
+ * never encrypts, ping and pong, and acknowledgements — TS3 3.13 and TS6 6.0
1489
+ * both acknowledge commands with plaintext Acks once crypto init has
1490
+ * completed. An Ack carries nothing but the id it acknowledges, so a forged
1491
+ * one can at worst stop a resend, which dropping the packet would do too.
1492
+ * Init1 is handled before decryption.
1493
+ */
1494
+ var UNENCRYPTED_TYPES = /* @__PURE__ */ new Set([
1495
+ PacketType.Voice,
1496
+ PacketType.VoiceWhisper,
1497
+ PacketType.Ping,
1498
+ PacketType.Pong,
1499
+ PacketType.Ack,
1500
+ PacketType.AckLow,
1501
+ PacketType.Init1
1502
+ ]);
1503
+ var PacketHandler = class {
1504
+ onPacket = null;
1505
+ onClosed = null;
1506
+ /**
1507
+ * A received datagram could not be handled: it was malformed, or handling it
1508
+ * threw. The datagram is dropped either way; the owner decides whether that
1509
+ * ends the connection. Without an owner the error is logged.
1510
+ */
1511
+ onReceiveError = null;
1512
+ #crypt;
1513
+ #logger;
1514
+ #socketFactory;
1515
+ #conn = null;
1516
+ #clientID = 0;
1517
+ #closed = false;
1518
+ #stopPing = null;
1519
+ #stopResend = null;
1520
+ #lastMessageReceived = Date.now();
1521
+ #packetCounter = /* @__PURE__ */ new Uint16Array(9);
1522
+ #generationCounter = /* @__PURE__ */ new Uint32Array(9);
1523
+ #recvWindowCommand = new GenerationWindow(65536, RECEIVE_PACKET_WINDOW_SIZE);
1524
+ #recvWindowCommandLow = new GenerationWindow(65536, RECEIVE_PACKET_WINDOW_SIZE);
1525
+ #sendWindowCommand = new GenerationWindow(65536, RECEIVE_PACKET_WINDOW_SIZE);
1526
+ #sendWindowCommandLow = new GenerationWindow(65536, RECEIVE_PACKET_WINDOW_SIZE);
1527
+ #commandQueue = /* @__PURE__ */ new Map();
1528
+ #commandLowQueue = /* @__PURE__ */ new Map();
1529
+ /** The bytes held in both command queues; see MAX_QUEUED_BYTES. */
1530
+ #queuedBytes = 0;
1531
+ #ackManager = /* @__PURE__ */ new Map();
1532
+ #initPacketCheck = null;
1533
+ /**
1534
+ * Command packets sent under the dummy key, before crypto init completed:
1535
+ * clientek, in practice. Both TS3 and TS6 acknowledge clientek under the
1536
+ * dummy key even though it completes crypto init, so the dummy key stays
1537
+ * acceptable for exactly those acknowledgements, and for nothing else.
1538
+ */
1539
+ #dummyCommandIDs = /* @__PURE__ */ new Set();
1540
+ #resends = 0;
1541
+ constructor(crypt, logger = noopLogger, socketFactory) {
1542
+ this.#crypt = crypt;
1543
+ this.#logger = logger;
1544
+ this.#socketFactory = socketFactory;
1545
+ }
1546
+ setClientID(id) {
1547
+ this.#clientID = id;
1548
+ }
1549
+ /**
1550
+ * Resolve `addr`, open a socket to it and start the handshake.
1551
+ *
1552
+ * Aborting `signal`, or closing the handler, while this runs abandons it: the
1553
+ * lookup's answer is ignored, and a socket that finishes connecting
1554
+ * afterwards is closed rather than started, so no handshake begins after
1555
+ * the connection was given up.
1556
+ */
1557
+ async connect(addr, signal) {
1558
+ const { host, portNumber } = parseConnectAddress(addr);
1559
+ this.#throwIfAbandoned(signal);
1560
+ let resolved;
1561
+ try {
1562
+ resolved = await abortable(lookup(host), signal);
1563
+ } catch (error) {
1564
+ if (signal?.aborted) throw error;
1565
+ throw new TeamspeakError(`could not resolve ${host}`, { cause: error });
1566
+ }
1567
+ const { address, family } = resolved;
1568
+ this.#throwIfAbandoned(signal);
1569
+ const family6 = family === 6;
1570
+ const socket = this.#socketFactory ? this.#socketFactory(family6 ? 6 : 4) : createSocket(family6 ? "udp6" : "udp4");
1571
+ await new Promise((resolve, reject) => {
1572
+ let done = false;
1573
+ const fail = (error) => {
1574
+ if (done) return;
1575
+ done = true;
1576
+ socket.off("error", onError);
1577
+ signal?.removeEventListener("abort", onAbort);
1578
+ closeQuietly(socket);
1579
+ reject(error);
1580
+ };
1581
+ const onError = (error) => fail(error);
1582
+ const onAbort = () => fail(abortReason(signal));
1583
+ socket.once("error", onError);
1584
+ signal?.addEventListener("abort", onAbort, { once: true });
1585
+ socket.connect(portNumber, address, () => {
1586
+ if (done) return;
1587
+ if (this.#closed || signal?.aborted) {
1588
+ fail(signal?.aborted ? abortReason(signal) : new NotConnectedError());
1589
+ return;
1590
+ }
1591
+ done = true;
1592
+ socket.off("error", onError);
1593
+ signal?.removeEventListener("abort", onAbort);
1594
+ this.start(socket);
1595
+ resolve();
1596
+ });
1597
+ });
1598
+ }
1599
+ #throwIfAbandoned(signal) {
1600
+ signal?.throwIfAborted();
1601
+ if (this.#closed) throw new NotConnectedError();
1602
+ }
1603
+ /** Take over a connected socket and start the handshake on it. A closed handler closes it instead. */
1604
+ start(conn) {
1605
+ if (this.#closed) {
1606
+ closeQuietly(conn);
1607
+ return;
1608
+ }
1609
+ this.#conn = conn;
1610
+ this.#lastMessageReceived = Date.now();
1611
+ conn.on("message", (msg) => {
1612
+ if (this.#closed) return;
1613
+ try {
1614
+ this.#handleRawPacket(new Uint8Array(msg.buffer, msg.byteOffset, msg.byteLength));
1615
+ } catch (error) {
1616
+ if (this.onReceiveError) this.onReceiveError(error);
1617
+ else this.#logger.debug("dropped a malformed packet", { error: String(error) });
1618
+ }
1619
+ });
1620
+ conn.on("error", (err) => {
1621
+ this.#logger.error("udp error", err);
1622
+ this.#triggerClose(err);
1623
+ });
1624
+ conn.on("close", () => this.#triggerClose(null));
1625
+ this.#packetCounter[PacketType.Command] = 1;
1626
+ this.#packetCounter[PacketType.Init1] = 101;
1627
+ const init1Data = processInit1(this.#crypt, null);
1628
+ if (init1Data) this.#sendPacketRaw(PacketType.Init1, init1Data, 0);
1629
+ const pingTimer = setInterval(() => {
1630
+ if (this.#crypt.cryptoInitComplete) this.sendPacket(PacketType.Ping, /* @__PURE__ */ new Uint8Array(0), PacketFlags.Unencrypted);
1631
+ }, PING_INTERVAL_MS);
1632
+ this.#stopPing = () => clearInterval(pingTimer);
1633
+ const resendTimer = setInterval(() => this.#checkResends(), RESEND_LOOP_INTERVAL_MS);
1634
+ this.#stopResend = () => clearInterval(resendTimer);
1635
+ }
1636
+ receivedFinalInitAck() {
1637
+ this.#initPacketCheck = null;
1638
+ }
1639
+ sendPacket(pType, data, flags) {
1640
+ const dummy = !this.#crypt.cryptoInitComplete;
1641
+ if (data.length > 487 && pType !== PacketType.Voice && pType !== PacketType.VoiceWhisper) {
1642
+ this.#sendSplitPacket(pType, data, flags, dummy);
1643
+ return;
1644
+ }
1645
+ this.#sendPacketRaw(pType, data, flags, dummy);
1646
+ }
1647
+ sendVoicePacket(data, codec) {
1648
+ const pID = this.#packetCounter[PacketType.Voice];
1649
+ const pGen = this.#generationCounter[PacketType.Voice];
1650
+ this.#packetCounter[PacketType.Voice] = pID + 1 & 65535;
1651
+ if (this.#packetCounter[PacketType.Voice] === 0) this.#generationCounter[PacketType.Voice]++;
1652
+ const payloadLen = VOICE_HEADER_SIZE + data.length;
1653
+ const voicePayload = new Uint8Array(payloadLen);
1654
+ new DataView(voicePayload.buffer).setUint16(0, pID, false);
1655
+ voicePayload[2] = codec;
1656
+ voicePayload.set(data, VOICE_HEADER_SIZE);
1657
+ const header = buildC2SHeader({
1658
+ typeFlagged: PacketType.Voice | PacketFlags.Unencrypted,
1659
+ id: pID,
1660
+ clientID: this.#clientID,
1661
+ generationID: pGen,
1662
+ data: voicePayload,
1663
+ receivedAt: 0
1664
+ });
1665
+ const final = new Uint8Array(13 + payloadLen);
1666
+ final.set(this.#crypt.fakeSignature, 0);
1667
+ final.set(header, TAG_SIZE);
1668
+ final.set(voicePayload, 13);
1669
+ this.#write(final);
1670
+ }
1671
+ /** Send an unencrypted TeamSpeak voice whisper packet to up to 32 clients. */
1672
+ sendWhisperPacket(data, targetClientIds, codec) {
1673
+ if (targetClientIds.length === 0) throw new RangeError("a whisper needs at least one target");
1674
+ if (targetClientIds.length > 32) throw new RangeError("a whisper has at most 32 targets");
1675
+ if (targetClientIds.some((id) => !Number.isInteger(id) || id < 0 || id > 65535)) throw new RangeError("invalid whisper target");
1676
+ const pID = this.#packetCounter[PacketType.VoiceWhisper];
1677
+ const pGen = this.#generationCounter[PacketType.VoiceWhisper];
1678
+ this.#packetCounter[PacketType.VoiceWhisper] = pID + 1 & 65535;
1679
+ if (this.#packetCounter[PacketType.VoiceWhisper] === 0) this.#generationCounter[PacketType.VoiceWhisper] = pGen + 1 >>> 0;
1680
+ const payloadLen = WHISPER_HEADER_SIZE + targetClientIds.length * 2 + data.length;
1681
+ const whisperPayload = new Uint8Array(payloadLen);
1682
+ const view = new DataView(whisperPayload.buffer);
1683
+ view.setUint16(0, pID, false);
1684
+ whisperPayload[2] = codec;
1685
+ whisperPayload[3] = 0;
1686
+ whisperPayload[4] = targetClientIds.length;
1687
+ for (let index = 0; index < targetClientIds.length; index++) view.setUint16(WHISPER_HEADER_SIZE + index * 2, targetClientIds[index], false);
1688
+ whisperPayload.set(data, WHISPER_HEADER_SIZE + targetClientIds.length * 2);
1689
+ const header = buildC2SHeader({
1690
+ typeFlagged: PacketType.VoiceWhisper | PacketFlags.Unencrypted,
1691
+ id: pID,
1692
+ clientID: this.#clientID,
1693
+ generationID: pGen,
1694
+ data: whisperPayload,
1695
+ receivedAt: 0
1696
+ });
1697
+ const final = new Uint8Array(13 + payloadLen);
1698
+ final.set(this.#crypt.fakeSignature, 0);
1699
+ final.set(header, TAG_SIZE);
1700
+ final.set(whisperPayload, 13);
1701
+ this.#write(final);
1702
+ }
1703
+ /** How many packets have been sent again for want of an acknowledgement. */
1704
+ get resends() {
1705
+ return this.#resends;
1706
+ }
1707
+ /** Whether there is a live socket to send on. */
1708
+ get isOpen() {
1709
+ return this.#conn !== null && !this.#closed;
1710
+ }
1711
+ close() {
1712
+ if (this.#closed) return;
1713
+ this.#closed = true;
1714
+ this.#stopPing?.();
1715
+ this.#stopResend?.();
1716
+ const conn = this.#conn;
1717
+ this.#conn = null;
1718
+ if (conn === null) return;
1719
+ try {
1720
+ conn.close();
1721
+ } catch (error) {
1722
+ this.#logger.debug("socket already closed", { error: String(error) });
1723
+ }
1724
+ }
1725
+ #sendSplitPacket(pType, data, flags, dummy) {
1726
+ const maxSize = 487;
1727
+ let pos = 0;
1728
+ let first = true;
1729
+ while (pos < data.length) {
1730
+ const blockSize = Math.min(data.length - pos, maxSize);
1731
+ const last = pos + blockSize === data.length;
1732
+ let pFlags = flags;
1733
+ if (first !== last) pFlags |= PacketFlags.Fragmented;
1734
+ this.#sendPacketRaw(pType, data.slice(pos, pos + blockSize), pFlags, dummy);
1735
+ pos += blockSize;
1736
+ first = false;
1737
+ }
1738
+ }
1739
+ #sendPacketRaw(pType, data, flags, dummy = !this.#crypt.cryptoInitComplete) {
1740
+ flags = applyProtocolFlags(pType, flags);
1741
+ const [pID, pGen] = this.#nextPacketIdentity(pType);
1742
+ const p = {
1743
+ typeFlagged: pType | flags,
1744
+ id: pID,
1745
+ clientID: this.#clientID,
1746
+ generationID: pGen,
1747
+ data,
1748
+ receivedAt: 0
1749
+ };
1750
+ const unencrypted = (flags & PacketFlags.Unencrypted) !== 0;
1751
+ const header = buildC2SHeader(p);
1752
+ const [ciphertext, tag] = this.#crypt.encrypt(pType, pID, pGen, header, data, dummy, unencrypted);
1753
+ const final = new Uint8Array(13 + ciphertext.length);
1754
+ final.set(tag.slice(0, TAG_SIZE), 0);
1755
+ final.set(header, TAG_SIZE);
1756
+ final.set(ciphertext, 13);
1757
+ this.#write(final);
1758
+ if (dummy && pType === PacketType.Command) this.#dummyCommandIDs.add(pID);
1759
+ const rp = {
1760
+ packet: p,
1761
+ firstSend: Date.now(),
1762
+ lastSend: Date.now(),
1763
+ retryCount: 0,
1764
+ nextInterval: RESEND_BASE_INTERVAL_MS
1765
+ };
1766
+ this.#trackResend(pType, p, rp);
1767
+ }
1768
+ #write(data) {
1769
+ if (!this.#conn) return;
1770
+ try {
1771
+ this.#conn.send(data, (err) => {
1772
+ if (err) this.#logger.warn("udp send error", err);
1773
+ });
1774
+ } catch (error) {
1775
+ const normalized = error instanceof Error ? error : new Error(String(error));
1776
+ this.#logger.error("udp send failed", normalized);
1777
+ this.#triggerClose(normalized);
1778
+ }
1779
+ }
1780
+ #handleRawPacket(raw) {
1781
+ if (raw.length < 11) return;
1782
+ if (raw.length > 4096) {
1783
+ this.#logger.debug("dropped an oversized datagram", { size: raw.length });
1784
+ return;
1785
+ }
1786
+ const tag = raw.slice(0, TAG_SIZE);
1787
+ const header = raw.slice(TAG_SIZE, 11);
1788
+ const ciphertext = raw.slice(11);
1789
+ const parsed = parseS2CHeader(header);
1790
+ if ((parsed.typeFlagged & 15) === PacketType.Init1 && this.#crypt.cryptoInitComplete) return;
1791
+ const p = {
1792
+ ...parsed,
1793
+ clientID: 0,
1794
+ generationID: this.#resolveGeneration(parsed.id, parsed.typeFlagged & 15),
1795
+ data: /* @__PURE__ */ new Uint8Array(0),
1796
+ receivedAt: Date.now()
1797
+ };
1798
+ const decrypted = this.#decryptPacketData(p, header, ciphertext, tag);
1799
+ if (decrypted === null) return;
1800
+ p.data = decrypted.plaintext;
1801
+ this.#lastMessageReceived = Date.now();
1802
+ if (packetType(p) === PacketType.Ping) {
1803
+ this.#sendPong(p.id, decrypted.dummyUsed);
1804
+ return;
1805
+ }
1806
+ if (!this.#handleCommandWindowAndAck(p, decrypted.dummyUsed)) return;
1807
+ this.#handlePacketQueue(p);
1808
+ this.#updatePostReceiveState(p);
1809
+ }
1810
+ #resolveGeneration(id, pType) {
1811
+ switch (pType) {
1812
+ case PacketType.Command: return this.#recvWindowCommand.getGeneration(id);
1813
+ case PacketType.CommandLow: return this.#recvWindowCommandLow.getGeneration(id);
1814
+ case PacketType.Ack: return this.#sendWindowCommand.getGeneration(id);
1815
+ case PacketType.AckLow: return this.#sendWindowCommandLow.getGeneration(id);
1816
+ default: return 0;
1817
+ }
1818
+ }
1819
+ #decryptPacketData(p, header, ciphertext, tag) {
1820
+ const unencrypted = (packetFlags(p) & PacketFlags.Unencrypted) !== 0;
1821
+ const dummy = !this.#crypt.cryptoInitComplete;
1822
+ let dummyUsed = dummy;
1823
+ const pType = packetType(p);
1824
+ const gen = p.generationID;
1825
+ if (unencrypted && !UNENCRYPTED_TYPES.has(pType)) {
1826
+ this.#logger.debug("dropped an unencrypted packet of a type that must be encrypted", {
1827
+ type: pType,
1828
+ id: p.id
1829
+ });
1830
+ return null;
1831
+ }
1832
+ try {
1833
+ return {
1834
+ plaintext: this.#crypt.decrypt(pType, p.id, gen, header, ciphertext, tag, dummy, unencrypted),
1835
+ dummyUsed
1836
+ };
1837
+ } catch {
1838
+ if (dummy) return null;
1839
+ if (!dummy && pType === PacketType.Ack && !unencrypted) try {
1840
+ const plaintext = this.#crypt.decrypt(pType, p.id, gen, header, ciphertext, tag, true, false);
1841
+ const acked = plaintext.length >= 2 ? plaintext[0] << 8 | plaintext[1] : NaN;
1842
+ if (this.#dummyCommandIDs.delete(acked)) return {
1843
+ plaintext,
1844
+ dummyUsed: true
1845
+ };
1846
+ } catch {}
1847
+ return null;
1848
+ }
1849
+ }
1850
+ #handleCommandWindowAndAck(p, dummyUsed) {
1851
+ const pType = packetType(p);
1852
+ if (pType !== PacketType.Command && pType !== PacketType.CommandLow) return true;
1853
+ const win = pType === PacketType.Command ? this.#recvWindowCommand : this.#recvWindowCommandLow;
1854
+ const ackType = pType === PacketType.Command ? PacketType.Ack : PacketType.AckLow;
1855
+ if (!win.isInWindow(p.id)) {
1856
+ if (win.isOldPacket(p.id)) this.#sendAck(p.id, ackType, dummyUsed);
1857
+ return false;
1858
+ }
1859
+ const held = (pType === PacketType.Command ? this.#commandQueue : this.#commandLowQueue).get(p.id)?.data.length ?? 0;
1860
+ const next = pType === PacketType.Command ? this.#_nextCommandID : this.#_nextCommandLowID;
1861
+ if (p.id !== next && this.#queuedBytes - held + p.data.length > 1048576) {
1862
+ this.#logger.debug("dropped a command packet: the receive queues are full", { id: p.id });
1863
+ return false;
1864
+ }
1865
+ this.#sendAck(p.id, ackType, dummyUsed);
1866
+ return true;
1867
+ }
1868
+ #sendAck(packetID, ackType, dummyUsed) {
1869
+ const ackData = /* @__PURE__ */ new Uint8Array(2);
1870
+ new DataView(ackData.buffer).setUint16(0, packetID, false);
1871
+ this.#sendPacketRaw(ackType, ackData, 0, dummyUsed);
1872
+ }
1873
+ #sendPong(pID, dummyUsed) {
1874
+ const pongData = /* @__PURE__ */ new Uint8Array(2);
1875
+ new DataView(pongData.buffer).setUint16(0, pID, false);
1876
+ this.#sendPacketRaw(PacketType.Pong, pongData, PacketFlags.Unencrypted, dummyUsed);
1877
+ }
1878
+ #handlePacketQueue(p) {
1879
+ const pType = packetType(p);
1880
+ if (pType !== PacketType.Command && pType !== PacketType.CommandLow) {
1881
+ this.onPacket?.(p);
1882
+ return;
1883
+ }
1884
+ const isCommand = pType === PacketType.Command;
1885
+ const queue = isCommand ? this.#commandQueue : this.#commandLowQueue;
1886
+ const win = isCommand ? this.#recvWindowCommand : this.#recvWindowCommandLow;
1887
+ this.#hold(queue, p);
1888
+ if (isCommand) this.#fastForwardMissingPackets(queue, win, true);
1889
+ else this.#fastForwardMissingPackets(queue, win, false);
1890
+ while (true) {
1891
+ const current = isCommand ? this.#_nextCommandID : this.#_nextCommandLowID;
1892
+ const packet = queue.get(current);
1893
+ if (packet === void 0) break;
1894
+ const result = this.#tryReassemble(packet, queue, current, win);
1895
+ if (!result) break;
1896
+ const [reassembled, newNext] = result;
1897
+ if (isCommand) this.#_nextCommandID = newNext;
1898
+ else this.#_nextCommandLowID = newNext;
1899
+ if (reassembled === null) {
1900
+ this.#logger.debug("dropped a command over the reassembly limit", { id: current });
1901
+ continue;
1902
+ }
1903
+ if (!this.#tryDecompress(reassembled)) continue;
1904
+ this.onPacket?.(reassembled);
1905
+ }
1906
+ }
1907
+ /** Put a packet in its queue, counting its bytes; one with the same id is replaced. */
1908
+ #hold(queue, p) {
1909
+ this.#release(queue, p.id);
1910
+ queue.set(p.id, p);
1911
+ this.#queuedBytes += p.data.length;
1912
+ }
1913
+ #release(queue, id) {
1914
+ const held = queue.get(id);
1915
+ if (held === void 0) return;
1916
+ queue.delete(id);
1917
+ this.#queuedBytes -= held.data.length;
1918
+ }
1919
+ #_nextCommandID = 0;
1920
+ #_nextCommandLowID = 0;
1921
+ #fastForwardMissingPackets(queue, win, isCommand) {
1922
+ let nextID = isCommand ? this.#_nextCommandID : this.#_nextCommandLowID;
1923
+ while (!queue.has(nextID) && hasOldNewerPacket(queue, nextID)) {
1924
+ nextID = nextID + 1 & 65535;
1925
+ win.advance(1);
1926
+ }
1927
+ if (isCommand) this.#_nextCommandID = nextID;
1928
+ else this.#_nextCommandLowID = nextID;
1929
+ }
1930
+ #tryReassemble(startPacket, queue, nextID, win) {
1931
+ if ((packetFlags(startPacket) & PacketFlags.Fragmented) === 0) {
1932
+ this.#release(queue, nextID);
1933
+ win.advance(1);
1934
+ return [startPacket, nextID + 1 & 65535];
1935
+ }
1936
+ const fragments = [];
1937
+ let totalSize = 0;
1938
+ let currID = nextID;
1939
+ let startSeen = false;
1940
+ let complete = false;
1941
+ while (true) {
1942
+ const frag = queue.get(currID);
1943
+ if (!frag) return null;
1944
+ fragments.push(frag);
1945
+ totalSize += frag.data.length;
1946
+ if (totalSize > MAX_REASSEMBLED_SIZE) {
1947
+ for (let i = 0; i < fragments.length; i++) {
1948
+ this.#release(queue, nextID);
1949
+ win.advance(1);
1950
+ nextID = nextID + 1 & 65535;
1951
+ }
1952
+ return [null, nextID];
1953
+ }
1954
+ const fragmented = (packetFlags(frag) & PacketFlags.Fragmented) !== 0;
1955
+ if (!startSeen) {
1956
+ startSeen = true;
1957
+ if (!fragmented) {
1958
+ complete = true;
1959
+ break;
1960
+ }
1961
+ } else if (fragmented) {
1962
+ complete = true;
1963
+ break;
1964
+ }
1965
+ currID = currID + 1 & 65535;
1966
+ }
1967
+ if (!complete) return null;
1968
+ const combined = new Uint8Array(totalSize);
1969
+ let pos = 0;
1970
+ for (const frag of fragments) {
1971
+ combined.set(frag.data, pos);
1972
+ pos += frag.data.length;
1973
+ this.#release(queue, nextID);
1974
+ win.advance(1);
1975
+ nextID = nextID + 1 & 65535;
1976
+ }
1977
+ startPacket.data = combined;
1978
+ startPacket.typeFlagged &= ~PacketFlags.Fragmented;
1979
+ return [startPacket, nextID];
1980
+ }
1981
+ /** Decompress a command in place; false when it cannot be, and is to be dropped. */
1982
+ #tryDecompress(packet) {
1983
+ if ((packetFlags(packet) & PacketFlags.Compressed) === 0) return true;
1984
+ try {
1985
+ packet.data = new Qlz().decompress(packet.data);
1986
+ packet.typeFlagged &= ~PacketFlags.Compressed;
1987
+ return true;
1988
+ } catch (err) {
1989
+ this.#logger.debug("dropped a command that failed to decompress", {
1990
+ id: packet.id,
1991
+ error: String(err)
1992
+ });
1993
+ return false;
1994
+ }
1995
+ }
1996
+ #updatePostReceiveState(p) {
1997
+ const pType = packetType(p);
1998
+ if (pType === PacketType.Init1) {
1999
+ this.#initPacketCheck = null;
2000
+ return;
2001
+ }
2002
+ if ((pType === PacketType.Ack || pType === PacketType.AckLow) && p.data.length >= 2) {
2003
+ const ackID = new DataView(p.data.buffer, p.data.byteOffset).getUint16(0, false);
2004
+ const key = (pType === PacketType.Ack ? PacketType.Command : PacketType.CommandLow) << 16 | ackID;
2005
+ this.#ackManager.delete(key);
2006
+ }
2007
+ }
2008
+ #nextPacketIdentity(pType) {
2009
+ const pID = this.#packetCounter[pType];
2010
+ const pGen = this.#generationCounter[pType];
2011
+ if (pType === PacketType.Init1) return [pID, pGen];
2012
+ this.#packetCounter[pType] = pID + 1 & 65535;
2013
+ if (this.#packetCounter[pType] === 0) this.#generationCounter[pType] = pGen + 1 >>> 0;
2014
+ if (pType === PacketType.Command) this.#sendWindowCommand.advanceToExcluded(pID);
2015
+ else if (pType === PacketType.CommandLow) this.#sendWindowCommandLow.advanceToExcluded(pID);
2016
+ return [pID, pGen];
2017
+ }
2018
+ #trackResend(pType, p, rp) {
2019
+ if (pType === PacketType.Init1) {
2020
+ this.#initPacketCheck = rp;
2021
+ return;
2022
+ }
2023
+ if (pType === PacketType.Command || pType === PacketType.CommandLow) {
2024
+ const key = pType << 16 | p.id;
2025
+ this.#ackManager.set(key, rp);
2026
+ }
2027
+ }
2028
+ #checkResends() {
2029
+ const now = Date.now();
2030
+ if (now - this.#lastMessageReceived > PACKET_TIMEOUT_MS) {
2031
+ this.#logger.warn("idle timeout");
2032
+ this.#triggerClose(new ConnectionTimeoutError());
2033
+ return;
2034
+ }
2035
+ if (this.#initPacketCheck) this.#doResend(this.#initPacketCheck, now);
2036
+ for (const [key, rp] of this.#ackManager) {
2037
+ if (now - rp.firstSend > PACKET_TIMEOUT_MS) {
2038
+ this.#ackManager.delete(key);
2039
+ this.#triggerClose(new ConnectionTimeoutError());
2040
+ return;
2041
+ }
2042
+ this.#doResend(rp, now);
2043
+ }
2044
+ }
2045
+ #doResend(rp, now) {
2046
+ if (now - rp.lastSend < rp.nextInterval) return;
2047
+ rp.lastSend = now;
2048
+ rp.retryCount++;
2049
+ this.#resends++;
2050
+ rp.nextInterval = Math.min(rp.nextInterval * 2, MAX_RETRY_INTERVAL_MS);
2051
+ const dummy = !this.#crypt.cryptoInitComplete;
2052
+ const unencrypted = (packetFlags(rp.packet) & PacketFlags.Unencrypted) !== 0;
2053
+ const header = buildC2SHeader(rp.packet);
2054
+ const pType = packetType(rp.packet);
2055
+ const [ciphertext, tag] = this.#crypt.encrypt(pType, rp.packet.id, rp.packet.generationID, header, rp.packet.data, dummy, unencrypted);
2056
+ const final = new Uint8Array(13 + ciphertext.length);
2057
+ final.set(tag.slice(0, TAG_SIZE), 0);
2058
+ final.set(header, TAG_SIZE);
2059
+ final.set(ciphertext, 13);
2060
+ this.#write(final);
2061
+ }
2062
+ #triggerClose(err) {
2063
+ if (this.#closed) return;
2064
+ this.close();
2065
+ this.onClosed?.(err);
2066
+ }
2067
+ };
2068
+ /** Close a socket that may already be closed; dgram throws on a second close. */
2069
+ function closeQuietly(socket) {
2070
+ try {
2071
+ socket.close();
2072
+ } catch {}
2073
+ }
2074
+ function applyProtocolFlags(pType, flags) {
2075
+ if (pType === PacketType.Command || pType === PacketType.CommandLow) return flags | PacketFlags.NewProtocol;
2076
+ return flags;
2077
+ }
2078
+ function hasOldNewerPacket(queue, nextID) {
2079
+ const now = Date.now();
2080
+ for (const [id, pkg] of queue) if ((id - nextID + 65536 & 65535) < 32768 && now - pkg.receivedAt > 5e3) return true;
2081
+ return false;
2082
+ }
2083
+ //#endregion
2084
+ //#region src/commands.ts
2085
+ /**
2086
+ * Matches the server's answers to the commands that asked for them.
2087
+ *
2088
+ * The server handles one client's commands in order and closes each answer
2089
+ * with `error … return_code=N`, so every row seen since the previous `error`
2090
+ * belongs to command N. Rows are therefore held, tagged with where they came
2091
+ * from, until that `error` says whose they are — attributing them on arrival
2092
+ * would mean guessing which in-flight command the server is answering, and a
2093
+ * command can be registered here well before it is sent (it first waits on the
2094
+ * throttle and the middleware chain).
2095
+ *
2096
+ * A nameless row always belongs to the command. A notification row belongs to
2097
+ * it only if the command declared that notification as its answer (see
2098
+ * command/responses.ts): the same `notifyclientmoved` that answers nothing can
2099
+ * arrive between any two commands.
2100
+ *
2101
+ * The welcome sequence is the exception. The server pushes channellist,
2102
+ * channelgrouplist and the rest unprompted right after the handshake, before
2103
+ * `channellistfinished`; without the gate they would land in the first
2104
+ * command's answer as duplicate rows and a phantom cid=0.
2105
+ */
2106
+ var CommandTracker = class {
2107
+ #pending = /* @__PURE__ */ new Map();
2108
+ #ownClientID;
2109
+ #nextRC = 0;
2110
+ #buffer = [];
2111
+ #welcomeComplete = false;
2112
+ /** @param ownClientID the client's clid, for answers filtered by invoker. */
2113
+ constructor(ownClientID = () => 0) {
2114
+ this.#ownClientID = ownClientID;
2115
+ }
2116
+ register(spec, correlation) {
2117
+ this.#nextRC++;
2118
+ const rc = this.#nextRC;
2119
+ return [rc, new Promise((resolve) => {
2120
+ this.#pending.set(rc, {
2121
+ resolve,
2122
+ spec,
2123
+ correlation,
2124
+ correlated: []
2125
+ });
2126
+ })];
2127
+ }
2128
+ unregister(rc) {
2129
+ this.#pending.delete(rc);
2130
+ }
2131
+ /** Marks the end of the welcome sequence and drops what it left behind. */
2132
+ signalWelcomeComplete() {
2133
+ this.#welcomeComplete = true;
2134
+ this.#buffer = [];
2135
+ }
2136
+ /** Whether some in-flight command declared `name` as its answer. */
2137
+ expects(name) {
2138
+ for (const p of this.#pending.values()) {
2139
+ if (p.spec?.notifications?.includes(name)) return true;
2140
+ if (p.spec?.correlated?.notifications.includes(name)) return true;
2141
+ }
2142
+ return false;
2143
+ }
2144
+ /** Hold a row until the next `error` says which command it answered. */
2145
+ buffer(params, source = "") {
2146
+ if (!this.#welcomeComplete) return;
2147
+ if (this.#pending.size === 0) return;
2148
+ this.#buffer.push({
2149
+ source,
2150
+ params
2151
+ });
2152
+ }
2153
+ /**
2154
+ * Offer a notification to commands answered after their `error`.
2155
+ *
2156
+ * Matching is on the echoed request field, so it does not matter whether the
2157
+ * notification beats the `error` or follows it.
2158
+ */
2159
+ offerCorrelated(name, params) {
2160
+ for (const [rc, p] of this.#pending) {
2161
+ const c = p.spec?.correlated;
2162
+ if (!c || p.correlation === void 0 || !c.notifications.includes(name)) continue;
2163
+ if (params[c.key] !== p.correlation) continue;
2164
+ if (name === c.failure && (params["status"] ?? "0") !== "0") {
2165
+ const err = new ServerError(parseErrorCode(params["status"]), params["msg"] ?? "", { extraMessage: params["extra_msg"] || void 0 });
2166
+ this.#settle(rc, p, {
2167
+ err,
2168
+ data: []
2169
+ });
2170
+ return;
2171
+ }
2172
+ p.correlated.push(params);
2173
+ if (p.settled) this.#settle(rc, p, {
2174
+ err: null,
2175
+ data: [...p.settled.data, ...p.correlated]
2176
+ });
2177
+ return;
2178
+ }
2179
+ }
2180
+ resolve(rc, err) {
2181
+ const rows = this.#buffer;
2182
+ this.#buffer = [];
2183
+ const p = this.#pending.get(rc);
2184
+ if (!p) return;
2185
+ const data = rows.filter((row) => this.#answers(p, row)).map((row) => row.params);
2186
+ if (err) {
2187
+ this.#settle(rc, p, {
2188
+ err,
2189
+ data
2190
+ });
2191
+ return;
2192
+ }
2193
+ if (p.spec?.correlated && p.correlation !== void 0 && p.correlated.length === 0) {
2194
+ p.settled = {
2195
+ err: null,
2196
+ data
2197
+ };
2198
+ return;
2199
+ }
2200
+ this.#settle(rc, p, {
2201
+ err: null,
2202
+ data: [...data, ...p.correlated]
2203
+ });
2204
+ }
2205
+ discardBuffer() {
2206
+ this.#buffer = [];
2207
+ }
2208
+ /**
2209
+ * Forget every command and fail it with `err`.
2210
+ *
2211
+ * Dropping them silently would leave each caller waiting out its full
2212
+ * timeout for an answer that can no longer come.
2213
+ */
2214
+ reset(err) {
2215
+ const pending = [...this.#pending.values()];
2216
+ this.#pending.clear();
2217
+ for (const p of pending) p.resolve({
2218
+ err,
2219
+ data: []
2220
+ });
2221
+ this.#buffer = [];
2222
+ this.#welcomeComplete = false;
2223
+ this.#nextRC = 0;
2224
+ }
2225
+ #answers(p, row) {
2226
+ if (row.source === "") return true;
2227
+ if (!p.spec?.notifications?.includes(row.source)) return false;
2228
+ if (!p.spec.ownRowsOnly) return true;
2229
+ return row.params["invokerid"] === String(this.#ownClientID());
2230
+ }
2231
+ #settle(rc, p, result) {
2232
+ this.#pending.delete(rc);
2233
+ p.resolve(result);
2234
+ }
2235
+ };
2236
+ /**
2237
+ * Append the tracker's return_code to a command.
2238
+ *
2239
+ * The return_code belongs to the tracker: it is how an answer finds its
2240
+ * command, so a caller-supplied one would detach the command from its answer.
2241
+ * Checked as a parameter rather than as text, because a message body that
2242
+ * merely contains "return_code=" used to suppress the real one and time the
2243
+ * command out.
2244
+ */
2245
+ function appendReturnCode(cmd, rc) {
2246
+ const params = parseCommand(cmd)?.params ?? {};
2247
+ if (Object.hasOwn(params, "return_code")) throw new TypeError("commands must not carry their own return_code; the client assigns it");
2248
+ return `${cmd} return_code=${rc}`;
2249
+ }
2250
+ //#endregion
2251
+ //#region src/transfer.ts
2252
+ /**
2253
+ * File transfer data.
2254
+ *
2255
+ * Negotiating a transfer is a command (`initFileUpload`, `initFileDownload`);
2256
+ * the bytes then go over a separate TCP connection to the server's file
2257
+ * transfer port, opened with the key the negotiation returned.
2258
+ */
2259
+ /** How long dialing may take before it fails, unless a signal says otherwise. */
2260
+ var DIAL_TIMEOUT_MS = 1e4;
2261
+ /**
2262
+ * Open a connection to the file transfer port and send the transfer key.
2263
+ * The caller owns the returned stream and must end or destroy it.
2264
+ */
2265
+ async function dialFileTransfer(host, port, key, options = {}) {
2266
+ const { signal, socketFactory } = options;
2267
+ signal?.throwIfAborted();
2268
+ const socket = socketFactory ? socketFactory(host, port) : connect({
2269
+ host,
2270
+ port
2271
+ });
2272
+ const swallow = () => {};
2273
+ socket.on("error", swallow);
2274
+ try {
2275
+ await connected(socket, signal);
2276
+ await new Promise((resolve, reject) => {
2277
+ socket.write(key, (err) => err ? reject(new FileTransferError(`failed to send transfer key: ${err.message}`, { cause: err })) : resolve());
2278
+ });
2279
+ } catch (error) {
2280
+ socket.destroy();
2281
+ throw error;
2282
+ } finally {
2283
+ socket.off("error", swallow);
2284
+ }
2285
+ return socket;
2286
+ }
2287
+ /**
2288
+ * Wait for a stream to be connected. A `node:net` socket says so with
2289
+ * `connect`; a stream from elsewhere that is already writable is taken as
2290
+ * connected.
2291
+ */
2292
+ function connected(socket, signal) {
2293
+ const pending = socket.pending;
2294
+ if (pending === false || pending === void 0 && socket.writable) return Promise.resolve();
2295
+ return new Promise((resolve, reject) => {
2296
+ const timeout = AbortSignal.timeout(DIAL_TIMEOUT_MS);
2297
+ const done = (error) => {
2298
+ socket.off("connect", onConnect);
2299
+ socket.off("ready", onConnect);
2300
+ socket.off("error", onError);
2301
+ signal?.removeEventListener("abort", onAbort);
2302
+ timeout.removeEventListener("abort", onTimeout);
2303
+ if (error === void 0) resolve();
2304
+ else reject(error);
2305
+ };
2306
+ const onConnect = () => done();
2307
+ const onError = (error) => done(new FileTransferError(`file transfer connection failed: ${error.message}`, { cause: error }));
2308
+ const onAbort = () => done(signal.reason);
2309
+ const onTimeout = () => done(new FileTransferError("file transfer connection timed out"));
2310
+ socket.once("connect", onConnect);
2311
+ socket.once("ready", onConnect);
2312
+ socket.once("error", onError);
2313
+ signal?.addEventListener("abort", onAbort, { once: true });
2314
+ timeout.addEventListener("abort", onTimeout, { once: true });
2315
+ });
2316
+ }
2317
+ /** Upload `data` over a negotiated transfer. Resolves once every byte is sent. */
2318
+ async function uploadFileData(host, info, data, options = {}) {
2319
+ const socket = await dialFileTransfer(host, info.port, info.fileTransferKey, options);
2320
+ await pipeline(data, socket, options.signal ? { signal: options.signal } : {});
2321
+ }
2322
+ /** Download a negotiated transfer into `dest`. Resolves once the file is written. */
2323
+ async function downloadFileData(host, info, dest, options = {}) {
2324
+ const socket = await dialFileTransfer(host, info.port, info.fileTransferKey, options);
2325
+ await pipeline(socket, dest, options.signal ? { signal: options.signal } : {});
2326
+ }
2327
+ //#endregion
2328
+ //#region src/events.ts
2329
+ /**
2330
+ * Compose a chain of middlewares around a base handler.
2331
+ * Rightmost middleware wraps the base first.
2332
+ */
2333
+ function buildCommandChain(middlewares, base) {
2334
+ let handler = base;
2335
+ for (let i = middlewares.length - 1; i >= 0; i--) handler = middlewares[i](handler);
2336
+ return handler;
2337
+ }
2338
+ function buildEventChain(middlewares, base) {
2339
+ let handler = base;
2340
+ for (let i = middlewares.length - 1; i >= 0; i--) handler = middlewares[i](handler);
2341
+ return handler;
2342
+ }
2343
+ //#endregion
2344
+ //#region src/capabilities.ts
2345
+ /**
2346
+ * Read what a server says about itself in `initserver`, or a version string
2347
+ * alone, such as "6.0.0-beta13.1 [Build: 1]".
2348
+ */
2349
+ function parseServerCapabilities(initserver) {
2350
+ const params = typeof initserver === "string" ? { virtualserver_version: initserver } : initserver;
2351
+ const version = params["virtualserver_version"] ?? "";
2352
+ const parsed = Number.parseInt(version, 10);
2353
+ const major = Number.isNaN(parsed) ? 0 : parsed;
2354
+ let family = "unknown";
2355
+ if (major === 3) family = "ts3";
2356
+ else if (major >= 6) family = "ts6";
2357
+ return {
2358
+ version,
2359
+ major,
2360
+ family,
2361
+ uidAlgorithm: uidAlgorithmForVersion(version),
2362
+ name: params["virtualserver_name"] ?? "",
2363
+ iconID: parseIconID(params["virtualserver_icon_id"]),
2364
+ hostBannerImageURL: params["virtualserver_hostbanner_gfx_url"]?.trim() || void 0,
2365
+ welcomeMessage: params["virtualserver_welcomemessage"] ?? ""
2366
+ };
2367
+ }
2368
+ /**
2369
+ * Capabilities of a server that has not been connected to yet.
2370
+ *
2371
+ * The family is `unknown` — nothing has said which server this is — and
2372
+ * `uidAlgorithm` is SHA-1, the TS3 hash.
2373
+ */
2374
+ function unknownServerCapabilities() {
2375
+ return parseServerCapabilities("");
2376
+ }
2377
+ //#endregion
2378
+ //#region src/handshake/license.ts
2379
+ var LICENSE_ROOT_KEY = new Uint8Array([
2380
+ 205,
2381
+ 13,
2382
+ 226,
2383
+ 174,
2384
+ 212,
2385
+ 99,
2386
+ 69,
2387
+ 80,
2388
+ 154,
2389
+ 126,
2390
+ 60,
2391
+ 253,
2392
+ 143,
2393
+ 104,
2394
+ 179,
2395
+ 220,
2396
+ 117,
2397
+ 85,
2398
+ 178,
2399
+ 157,
2400
+ 204,
2401
+ 236,
2402
+ 115,
2403
+ 205,
2404
+ 24,
2405
+ 117,
2406
+ 15,
2407
+ 153,
2408
+ 56,
2409
+ 18,
2410
+ 64,
2411
+ 138
2412
+ ]);
2413
+ var LicenseChain = class {
2414
+ blocks;
2415
+ constructor(blocks) {
2416
+ this.blocks = blocks;
2417
+ }
2418
+ /**
2419
+ * Derive the session key by chaining Ed25519 point arithmetic starting from
2420
+ * the root key through each license block.
2421
+ */
2422
+ deriveKey() {
2423
+ let round = Uint8Array.from(LICENSE_ROOT_KEY);
2424
+ for (const block of this.blocks) round = deriveKeyFromBlock(block, round);
2425
+ return round;
2426
+ }
2427
+ };
2428
+ function parseLicenses(data) {
2429
+ if (data.length < 1) throw new Error("license too short");
2430
+ if (data[0] !== 1) throw new Error("unsupported license version");
2431
+ let remaining = data.slice(1);
2432
+ const blocks = [];
2433
+ while (remaining.length > 0) {
2434
+ if (blocks.length === 8) throw new Error(`license chain longer than 8 blocks`);
2435
+ const { block, consumed } = parseLicenseBlock(remaining);
2436
+ blocks.push(block);
2437
+ remaining = remaining.slice(consumed);
2438
+ }
2439
+ return new LicenseChain(blocks);
2440
+ }
2441
+ function parseLicenseBlock(data) {
2442
+ const MIN_BLOCK_LEN = 42;
2443
+ if (data.length < MIN_BLOCK_LEN) throw new Error("license too short");
2444
+ if (data[0] !== 0) throw new Error(`wrong key kind in license: ${data[0]}`);
2445
+ const blockType = data[33];
2446
+ const UNIX_OFFSET = 1356998400;
2447
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
2448
+ const notValidBefore = /* @__PURE__ */ new Date((view.getUint32(34, false) + UNIX_OFFSET) * 1e3);
2449
+ const notValidAfter = /* @__PURE__ */ new Date((view.getUint32(38, false) + UNIX_OFFSET) * 1e3);
2450
+ if (notValidAfter < notValidBefore) throw new Error("license times are invalid");
2451
+ const key = Uint8Array.from(data.slice(1, 33));
2452
+ const { payload, payloadRead } = parseBlockPayload(blockType, data, MIN_BLOCK_LEN);
2453
+ const allLen = MIN_BLOCK_LEN + payloadRead;
2454
+ if (allLen > data.length) throw new Error("license block longer than the license");
2455
+ const hashInput = data.slice(1, allLen);
2456
+ const hashFull = hash512(Uint8Array.from(hashInput));
2457
+ return {
2458
+ block: {
2459
+ key,
2460
+ hash: Uint8Array.from(hashFull.slice(0, 32)),
2461
+ properties: payload.properties,
2462
+ issuer: payload.issuer,
2463
+ notValidBefore,
2464
+ notValidAfter,
2465
+ blockType,
2466
+ serverType: payload.serverType
2467
+ },
2468
+ consumed: allLen
2469
+ };
2470
+ }
2471
+ function parseBlockPayload(blockType, data, minBlockLen) {
2472
+ switch (blockType) {
2473
+ case 0: {
2474
+ const { str, read } = readNullString(data.slice(46));
2475
+ return {
2476
+ payload: {
2477
+ issuer: str,
2478
+ serverType: 0,
2479
+ properties: [],
2480
+ read: 5 + read
2481
+ },
2482
+ payloadRead: 5 + read
2483
+ };
2484
+ }
2485
+ case 2: {
2486
+ const { str, read } = readNullString(data.slice(47));
2487
+ return {
2488
+ payload: {
2489
+ issuer: str,
2490
+ serverType: data[42] ?? 0,
2491
+ properties: [],
2492
+ read: 6 + read
2493
+ },
2494
+ payloadRead: 6 + read
2495
+ };
2496
+ }
2497
+ case 8: {
2498
+ const propCount = data[43] !== void 0 ? data[43] : 0;
2499
+ let pos = 44;
2500
+ const properties = [];
2501
+ for (let i = 0; i < propCount; i++) {
2502
+ if (pos >= data.length) throw new Error("license too short");
2503
+ const propLen = data[pos++];
2504
+ if (pos + propLen > data.length) throw new Error("license too short");
2505
+ properties.push(Uint8Array.from(data.slice(pos, pos + propLen)));
2506
+ pos += propLen;
2507
+ }
2508
+ return {
2509
+ payload: {
2510
+ issuer: "",
2511
+ serverType: data[42] ?? 0,
2512
+ properties,
2513
+ read: pos - minBlockLen
2514
+ },
2515
+ payloadRead: pos - minBlockLen
2516
+ };
2517
+ }
2518
+ case 32: return {
2519
+ payload: {
2520
+ issuer: "",
2521
+ serverType: 0,
2522
+ properties: [],
2523
+ read: 0
2524
+ },
2525
+ payloadRead: 0
2526
+ };
2527
+ default: throw new Error(`invalid license block type: ${blockType}`);
2528
+ }
2529
+ }
2530
+ /**
2531
+ * Scan for a null-terminated string and return its content and the index
2532
+ * of the null byte (NOT including it in the count, matching Go behaviour).
2533
+ * The parent formulas (`5 + read`, `6 + read`) already add 1 for the null.
2534
+ */
2535
+ function readNullString(data) {
2536
+ for (let i = 0; i < data.length; i++) if (data[i] === 0) return {
2537
+ str: new TextDecoder().decode(data.slice(0, i)),
2538
+ read: i
2539
+ };
2540
+ throw new Error("non-null-terminated issuer string");
2541
+ }
2542
+ /**
2543
+ * Derive the next key given a parent key and a license block.
2544
+ * Mirrors Go's licenseBlock.deriveKey.
2545
+ */
2546
+ function deriveKeyFromBlock(block, parent) {
2547
+ const scalarBytes = Uint8Array.from(block.hash);
2548
+ clampScalar(scalarBytes);
2549
+ const scalar = bytesToBigIntLE(scalarBytes);
2550
+ const negPub = ed25519.Point.fromBytes(block.key).negate();
2551
+ const negPar = ed25519.Point.fromBytes(parent).negate();
2552
+ const raw = scalarMultFull(negPub, scalar).add(negPar).toBytes();
2553
+ const final = new Uint8Array(raw.length);
2554
+ final.set(raw);
2555
+ final[31] = (final[31] !== void 0 ? final[31] : 0) ^ 128;
2556
+ return final;
2557
+ }
2558
+ //#endregion
2559
+ //#region src/handshake/crypt-init2.ts
2560
+ /**
2561
+ * CryptoInit2 performs the second stage of crypto initialization (Ed25519 ECDH).
2562
+ * Mirrors Go's handshake.CryptoInit2.
2563
+ */
2564
+ function cryptoInit2(crypt, license, omega, proof, beta, privateKey) {
2565
+ if (crypt.alpha.length === 0) throw new CryptoInitError("initivexpand2 arrived before clientinitiv was sent");
2566
+ const licenseBytes = Buffer.from(license, "base64");
2567
+ const omegaBytes = Buffer.from(omega, "base64");
2568
+ const proofBytes = Buffer.from(proof, "base64");
2569
+ const betaBytes = Buffer.from(beta, "base64");
2570
+ let serverPubKey;
2571
+ try {
2572
+ serverPubKey = importPublicKey(omegaBytes);
2573
+ } catch (error) {
2574
+ throw new CryptoInitError("the server's public key is invalid", { cause: error });
2575
+ }
2576
+ if (!verifySign(serverPubKey, licenseBytes, proofBytes)) throw new CryptoInitError("the server's license proof is not valid");
2577
+ let sharedSecret;
2578
+ try {
2579
+ sharedSecret = getSharedSecret2(parseLicenses(licenseBytes).deriveKey(), privateKey);
2580
+ } catch (error) {
2581
+ throw new CryptoInitError("the server's license is invalid", { cause: error });
2582
+ }
2583
+ crypt.setSharedSecret(crypt.alpha, betaBytes, sharedSecret);
2584
+ }
2585
+ //#endregion
2586
+ //#region src/handshake.ts
2587
+ /** Handle the `clientinitiv` message (P-256 based crypto path). */
2588
+ function handleHandshakeInitIV(client, params) {
2589
+ const alpha = params["alpha"] ?? "";
2590
+ const beta = params["beta"] ?? "";
2591
+ const omega = params["omega"] ?? "";
2592
+ client.crypt.initCrypto(alpha, beta, omega);
2593
+ client.logger.info("crypto initialized (P-256 path), sending clientinit");
2594
+ sendClientInit(client);
2595
+ }
2596
+ /** Handle the `initivexpand2` message (Ed25519 / TS3 crypto path). */
2597
+ function handleHandshakeExpand2(client, params) {
2598
+ client.logger.info("received initivexpand2");
2599
+ client.handler.receivedFinalInitAck();
2600
+ const license = params["l"] ?? "";
2601
+ const omega = params["omega"] ?? "";
2602
+ const proof = params["proof"] ?? "";
2603
+ const beta = params["beta"] ?? "";
2604
+ const privateKey = sendClientEkPacket(client, beta);
2605
+ cryptoInit2(client.crypt, license, omega, proof, beta, privateKey);
2606
+ sendClientInit(client);
2607
+ }
2608
+ /** Handle `initserver` — marks the client as connected. */
2609
+ function handleInitServer(client, params) {
2610
+ const idStr = params["aclid"] ?? params["clid"] ?? "";
2611
+ const clid = idStr ? parseInt(idStr, 10) : 0;
2612
+ if (clid > 0) {
2613
+ client.clid = clid;
2614
+ client.handler.setClientID(clid);
2615
+ }
2616
+ client.capabilities = parseServerCapabilities(params);
2617
+ client.logger.info("connected to server", { selfId: client.clid });
2618
+ client.markConnected();
2619
+ setImmediate(() => {
2620
+ const updateCmd = buildCommand("clientupdate", {
2621
+ client_input_muted: "0",
2622
+ client_output_muted: "0"
2623
+ });
2624
+ client.execNoWait(updateCmd).catch(() => {});
2625
+ });
2626
+ }
2627
+ function sendClientEkPacket(client, beta) {
2628
+ const [publicKey, privateKey] = generateTemporaryKey();
2629
+ const ekBase64 = Buffer.from(publicKey).toString("base64");
2630
+ const clientProof = buildClientEkProof(client, publicKey, beta);
2631
+ const clientEk = buildCommandOrdered("clientek", [["ek", ekBase64], ["proof", clientProof]]);
2632
+ client.handler.sendPacket(PacketType.Command, Buffer.from(clientEk), 0);
2633
+ return privateKey;
2634
+ }
2635
+ function buildClientEkProof(client, publicKey, beta) {
2636
+ const betaBytes = Buffer.from(beta, "base64");
2637
+ const toSign = /* @__PURE__ */ new Uint8Array(86);
2638
+ toSign.set(publicKey.slice(0, 32));
2639
+ toSign.set(betaBytes.slice(0, Math.min(54, betaBytes.length)), 32);
2640
+ const sig = sign(identityPrivateKey(client.crypt.identity), toSign);
2641
+ return Buffer.from(sig).toString("base64");
2642
+ }
2643
+ /**
2644
+ * Encode a password field for the command protocol.
2645
+ *
2646
+ * TeamSpeak expects password fields as base64(sha1(password)) — server and
2647
+ * channel passwords during the handshake, the `clientmove` channel password,
2648
+ * and the `cpw` field of the file transfer commands. Sending the plain text
2649
+ * instead makes the server reject correct passwords with error 781.
2650
+ */
2651
+ function prepareClientPassword(password) {
2652
+ if (password === "") return "";
2653
+ return createHash("sha1").update(password).digest().toString("base64");
2654
+ }
2655
+ function sendClientInit(client) {
2656
+ const cmd = buildClientInitCommand(client);
2657
+ client.handler.sendPacket(PacketType.Command, Buffer.from(cmd), 0);
2658
+ }
2659
+ function buildClientInitCommand(client) {
2660
+ const pubKeyBase64 = client.crypt.identity.publicKey();
2661
+ const initOptions = client.initOptions;
2662
+ const hwid = createHash("sha1").update(pubKeyBase64).digest().toString("base64");
2663
+ return buildCommandOrdered("clientinit", [
2664
+ ["client_nickname", client.nickname],
2665
+ ["client_version", "3.?.? [Build: 5680278000]"],
2666
+ ["client_platform", "Windows"],
2667
+ ["client_input_hardware", "1"],
2668
+ ["client_output_hardware", "1"],
2669
+ ["client_default_channel", initOptions.defaultChannel],
2670
+ ["client_default_channel_password", initOptions.defaultChannelPasswordHash],
2671
+ ["client_server_password", initOptions.serverPasswordHash],
2672
+ ["client_meta_data", ""],
2673
+ ["client_version_sign", "DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg=="],
2674
+ ["client_key_offset", String(client.crypt.identity.offset)],
2675
+ ["client_nickname_phonetic", ""],
2676
+ ["client_default_token", ""],
2677
+ ["hwid", hwid]
2678
+ ]);
2679
+ }
2680
+ //#endregion
2681
+ //#region src/notifications.ts
2682
+ function handleNotification(cmd, selfCLID, clients, _nickname) {
2683
+ switch (cmd.name) {
2684
+ case "notifycliententerview": return handleClientEnterView(cmd, clients);
2685
+ case "notifyclientleftview": return handleClientLeftView(cmd, selfCLID, clients);
2686
+ case "notifyclientmoved": return handleClientMoved(cmd, clients);
2687
+ case "notifytextmessage": return handleTextMessage(cmd, clients);
2688
+ case "notifyclientpoke": return handleClientPoked(cmd);
2689
+ default: return { kind: "unknown" };
2690
+ }
2691
+ }
2692
+ function handleClientEnterView(cmd, clients) {
2693
+ const info = parseClient(cmd.params);
2694
+ if (info.id === 0) return { kind: "unknown" };
2695
+ clients.set(info.id, info);
2696
+ return {
2697
+ kind: "clientEnter",
2698
+ info
2699
+ };
2700
+ }
2701
+ function handleClientLeftView(cmd, selfCLID, clients) {
2702
+ const clid = asClientID(parseUint16(cmd.params["clid"] ?? ""));
2703
+ if (clid === 0) return { kind: "unknown" };
2704
+ const isSelf = clid === selfCLID;
2705
+ clients.delete(clid);
2706
+ return {
2707
+ kind: "clientLeave",
2708
+ event: {
2709
+ clientID: clid,
2710
+ reason: parseInt10(cmd.params["reasonid"] ?? ""),
2711
+ reasonMessage: cmd.params["reasonmsg"] ?? "",
2712
+ invokerID: asClientID(parseUint16(cmd.params["invokerid"] ?? "")),
2713
+ invokerName: cmd.params["invokername"] ?? "",
2714
+ invokerUID: asUID(cmd.params["invokeruid"] ?? "")
2715
+ },
2716
+ isSelf
2717
+ };
2718
+ }
2719
+ function handleClientMoved(cmd, clients) {
2720
+ const clid = asClientID(parseUint16(cmd.params["clid"] ?? ""));
2721
+ const ctid = asChannelID(parseUint64(cmd.params["ctid"] ?? ""));
2722
+ if (clid === 0) return { kind: "unknown" };
2723
+ const existing = clients.get(clid);
2724
+ if (existing) clients.set(clid, {
2725
+ ...existing,
2726
+ channelID: ctid
2727
+ });
2728
+ return {
2729
+ kind: "clientMoved",
2730
+ event: {
2731
+ clientID: clid,
2732
+ targetChannelID: ctid,
2733
+ reason: parseInt10(cmd.params["reasonid"] ?? ""),
2734
+ invokerID: asClientID(parseUint16(cmd.params["invokerid"] ?? "")),
2735
+ invokerName: cmd.params["invokername"] ?? "",
2736
+ invokerUID: asUID(cmd.params["invokeruid"] ?? "")
2737
+ },
2738
+ reasonMessage: cmd.params["reasonmsg"] ?? ""
2739
+ };
2740
+ }
2741
+ function handleTextMessage(cmd, clients) {
2742
+ const invokerID = asClientID(parseUint16(cmd.params["invokerid"] ?? ""));
2743
+ const invokerInfo = clients.get(invokerID);
2744
+ return {
2745
+ kind: "textMessage",
2746
+ message: {
2747
+ targetMode: parseInt10(cmd.params["targetmode"] ?? ""),
2748
+ targetID: asClientID(parseUint16(cmd.params["target"] ?? "")),
2749
+ invokerID,
2750
+ invokerName: cmd.params["invokername"] ?? "",
2751
+ invokerUID: asUID(cmd.params["invokeruid"] ?? invokerInfo?.uid ?? ""),
2752
+ message: cmd.params["msg"] ?? "",
2753
+ invokerGroups: invokerInfo?.serverGroups ?? []
2754
+ }
2755
+ };
2756
+ }
2757
+ function handleClientPoked(cmd) {
2758
+ return {
2759
+ kind: "poked",
2760
+ event: {
2761
+ invokerID: asClientID(parseUint16(cmd.params["invokerid"] ?? "")),
2762
+ invokerName: cmd.params["invokername"] ?? "",
2763
+ invokerUID: asUID(cmd.params["invokeruid"] ?? ""),
2764
+ message: cmd.params["msg"] ?? ""
2765
+ }
2766
+ };
2767
+ }
2768
+ //#endregion
2769
+ //#region src/throttle.ts
2770
+ /**
2771
+ * Token-bucket limiter for outbound TS3 commands.
2772
+ * Mirrors Go's commandThrottle.
2773
+ *
2774
+ * Waiters are released in the order they arrived: each used to poll for a
2775
+ * token on its own timer, so a command that came later could take the turn of
2776
+ * one that had waited longer, and the commands went out of order.
2777
+ */
2778
+ var CommandThrottle = class CommandThrottle {
2779
+ static TOKEN_RATE = 4;
2780
+ static TOKEN_MAX = 8;
2781
+ #tokens = 5;
2782
+ #lastUpdate = Date.now();
2783
+ #waiters = [];
2784
+ #timer;
2785
+ /**
2786
+ * Wait for a token, behind every earlier waiter. Aborting `signal` rejects
2787
+ * with its reason and gives the place in line up; the abort listener is
2788
+ * removed however the wait ends.
2789
+ */
2790
+ wait(signal) {
2791
+ if (signal?.aborted) return Promise.reject(signal.reason);
2792
+ this.#refill();
2793
+ if (this.#waiters.length === 0 && this.#tokens >= 1) {
2794
+ this.#tokens -= 1;
2795
+ return Promise.resolve();
2796
+ }
2797
+ return new Promise((resolve, reject) => {
2798
+ const waiter = {
2799
+ resolve,
2800
+ signal,
2801
+ onAbort: () => {
2802
+ const index = this.#waiters.indexOf(waiter);
2803
+ if (index >= 0) this.#waiters.splice(index, 1);
2804
+ if (this.#waiters.length === 0) this.#stopTimer();
2805
+ reject(signal.reason);
2806
+ }
2807
+ };
2808
+ signal?.addEventListener("abort", waiter.onAbort, { once: true });
2809
+ this.#waiters.push(waiter);
2810
+ this.#schedule();
2811
+ });
2812
+ }
2813
+ #refill() {
2814
+ const now = Date.now();
2815
+ const elapsed = (now - this.#lastUpdate) / 1e3;
2816
+ this.#tokens = Math.min(this.#tokens + elapsed * CommandThrottle.TOKEN_RATE, CommandThrottle.TOKEN_MAX);
2817
+ this.#lastUpdate = now;
2818
+ }
2819
+ /** Arm the timer for when the next token is due, unless it already is. */
2820
+ #schedule() {
2821
+ if (this.#timer !== void 0 || this.#waiters.length === 0) return;
2822
+ const waitMs = this.#tokens >= 1 ? 0 : Math.ceil((1 - this.#tokens) / CommandThrottle.TOKEN_RATE * 1e3) + 10;
2823
+ this.#timer = setTimeout(() => {
2824
+ this.#timer = void 0;
2825
+ this.#release();
2826
+ }, waitMs);
2827
+ }
2828
+ #stopTimer() {
2829
+ clearTimeout(this.#timer);
2830
+ this.#timer = void 0;
2831
+ }
2832
+ /** Hand the tokens there are to the waiters at the head of the line. */
2833
+ #release() {
2834
+ this.#refill();
2835
+ while (this.#waiters.length > 0 && this.#tokens >= 1) {
2836
+ this.#tokens -= 1;
2837
+ const waiter = this.#waiters.shift();
2838
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
2839
+ waiter.resolve();
2840
+ }
2841
+ this.#schedule();
2842
+ }
2843
+ };
2844
+ //#endregion
2845
+ //#region src/helpers.ts
2846
+ /**
2847
+ * Reports whether `actual` equals `expected` or equals `expected` followed by
2848
+ * only digits — the pattern TeamSpeak uses when a nickname is already taken.
2849
+ */
2850
+ function isAutoNicknameMatch(expected, actual) {
2851
+ if (actual === expected) return true;
2852
+ if (!actual.startsWith(expected)) return false;
2853
+ const suffix = actual.slice(expected.length);
2854
+ return /^\d+$/.test(suffix);
2855
+ }
2856
+ //#endregion
2857
+ //#region src/diagnostics.ts
2858
+ /**
2859
+ * Diagnostics channels: the SDK's tracing points, for an application or an
2860
+ * APM agent (OpenTelemetry instruments `diagnostics_channel` directly) to
2861
+ * subscribe to.
2862
+ *
2863
+ * A library does not decide where its traces go, so the SDK publishes them
2864
+ * here, and with no subscriber a channel costs a property read.
2865
+ *
2866
+ * Messages carry the command's name, never its parameters: those hold
2867
+ * password hashes, which the server accepts as passwords, and privilege keys.
2868
+ *
2869
+ * ```ts
2870
+ * import { tracingChannel } from "node:diagnostics_channel";
2871
+ *
2872
+ * tracingChannel("honeybbq:teamspeak:command").subscribe({
2873
+ * start: ({ command }) => console.time(command),
2874
+ * asyncEnd: ({ command }) => console.timeEnd(command),
2875
+ * error: ({ command, error }) => console.error(command, error),
2876
+ * });
2877
+ * ```
2878
+ */
2879
+ /**
2880
+ * Traces every command with an answer: `start` when it is sent, `asyncEnd`
2881
+ * when its answer arrives, `error` when it fails.
2882
+ */
2883
+ var commandChannel = tracingChannel("honeybbq:teamspeak:command");
2884
+ /** Publishes every command the server sends. */
2885
+ var packetChannel = channel("honeybbq:teamspeak:packet");
2886
+ //#endregion
2887
+ //#region src/reconnect.ts
2888
+ /**
2889
+ * Reconnection policy.
2890
+ *
2891
+ * A dropped connection is normal on a UDP voice session, so a bot that cannot
2892
+ * recover is not usable unattended. Reconnecting is opt-in rather than
2893
+ * automatic: a silent reconnect makes a dropped session look like a healthy
2894
+ * one, and a caller that wanted to know about the drop never finds out.
2895
+ *
2896
+ * Backoff is not a nicety here. TeamSpeak bans an IP that opens several
2897
+ * connections in quick succession — measured at roughly three, after which the
2898
+ * server answers `error id=3329` and refuses everything. A reconnect loop
2899
+ * without a delay therefore bans itself, and the ban outlasts the outage it was
2900
+ * recovering from.
2901
+ */
2902
+ /** One second, doubling, capped at a minute, with 20% jitter, never giving up. */
2903
+ var defaultBackoffPolicy = Object.freeze({
2904
+ initialDelayMs: 1e3,
2905
+ maxDelayMs: 6e4,
2906
+ factor: 2,
2907
+ jitter: .2,
2908
+ maxAttempts: 0
2909
+ });
2910
+ /**
2911
+ * Compute the delay before an attempt.
2912
+ *
2913
+ * `attempt` is 1-based: the delay before the first retry is `initialDelayMs`,
2914
+ * give or take the jitter, and the delay never grows past `maxDelayMs`.
2915
+ */
2916
+ function backoffDelay(policy, attempt) {
2917
+ return jitteredDelay(policy, attempt, Math.random);
2918
+ }
2919
+ /** @internal The delay, with `random` supplying the jitter. */
2920
+ function jitteredDelay(policy, attempt, random) {
2921
+ const exponent = Math.max(0, attempt - 1);
2922
+ const raw = policy.initialDelayMs * policy.factor ** exponent;
2923
+ const capped = Math.min(raw, policy.maxDelayMs);
2924
+ if (policy.jitter <= 0) return Math.round(capped);
2925
+ const spread = capped * Math.min(policy.jitter, 1);
2926
+ const delay = capped + (random() * 2 - 1) * spread;
2927
+ return Math.round(Math.max(0, Math.min(delay, policy.maxDelayMs)));
2928
+ }
2929
+ /**
2930
+ * Server errors where retrying cannot help.
2931
+ *
2932
+ * Deliberately a short list of codes known to be refusals, rather than "any
2933
+ * ServerError". The server also answers transiently: after a dropped
2934
+ * connection it still holds the old session, and a reconnect with the same
2935
+ * identity is refused with 521 "too many clones already connected" until it
2936
+ * reaps the zombie — a condition that clears itself and is worth retrying.
2937
+ *
2938
+ * The asymmetry is deliberate. Retrying a permanent failure costs a few bounded
2939
+ * attempts; not retrying a transient one means never recovering.
2940
+ */
2941
+ var PERMANENT_SERVER_ERRORS = /* @__PURE__ */ new Set([
2942
+ ErrorCode.ConnectFailedBanned,
2943
+ ErrorCode.BanFlooding,
2944
+ ErrorCode.ServerInvalidPassword,
2945
+ ErrorCode.ChannelInvalidPassword,
2946
+ ErrorCode.PermissionsClientInsufficient
2947
+ ]);
2948
+ /**
2949
+ * Whether a failure is worth another attempt.
2950
+ *
2951
+ * Transport failures are retried; so is a server that stopped, and a server
2952
+ * error other than the known refusals — a ban, flooding, a wrong server or
2953
+ * channel password, missing permissions — since some, such as 521 "too many
2954
+ * clones" right after a drop, clear by themselves. A kick or a ban is not:
2955
+ * someone removed the client on purpose. Nor is an AlreadyConnectedError: the
2956
+ * client was connected by other means; nor an InvalidAddressError.
2957
+ */
2958
+ function defaultShouldRetry(error) {
2959
+ if (error instanceof AlreadyConnectedError) return false;
2960
+ if (error instanceof InvalidAddressError) return false;
2961
+ if (error instanceof RemovedError) return error.reason !== Reason.KickServer && error.reason !== Reason.KickServerBan;
2962
+ return !(error instanceof ServerError && PERMANENT_SERVER_ERRORS.has(error.code));
2963
+ }
2964
+ function resolveBackoffPolicy(options) {
2965
+ const policy = { ...defaultBackoffPolicy };
2966
+ for (const [key, value] of Object.entries(options.backoff ?? {})) if (value !== void 0) policy[key] = value;
2967
+ return policy;
2968
+ }
2969
+ //#endregion
2970
+ //#region src/client.ts
2971
+ function normalizeClientInitOptions(options) {
2972
+ return {
2973
+ serverPasswordHash: prepareClientPassword(options.serverPassword ?? ""),
2974
+ defaultChannel: options.defaultChannel ?? "",
2975
+ defaultChannelPasswordHash: prepareClientPassword(options.defaultChannelPassword ?? "")
2976
+ };
2977
+ }
2978
+ /** How long a command waits for its answer unless told otherwise. */
2979
+ var DEFAULT_COMMAND_TIMEOUT_MS = 1e4;
2980
+ /**
2981
+ * How long `disconnect()` waits for the server to acknowledge its goodbye
2982
+ * unless told otherwise: the acknowledgement is a courtesy, and a connection
2983
+ * that has already died would otherwise hold it for a full command timeout.
2984
+ */
2985
+ var DISCONNECT_TIMEOUT_MS = 1e3;
2986
+ /** How long each reconnection attempt may take. */
2987
+ var RECONNECT_ATTEMPT_TIMEOUT_MS = 3e4;
2988
+ function newConnectAttempt() {
2989
+ let resolve;
2990
+ let reject;
2991
+ const promise = new Promise((res, rej) => {
2992
+ resolve = res;
2993
+ reject = rej;
2994
+ });
2995
+ promise.catch(() => {});
2996
+ const controller = new AbortController();
2997
+ const attempt = {
2998
+ promise,
2999
+ signal: controller.signal,
3000
+ settled: false,
3001
+ resolve() {
3002
+ if (attempt.settled) return;
3003
+ attempt.settled = true;
3004
+ resolve();
3005
+ },
3006
+ reject(error) {
3007
+ if (attempt.settled) return;
3008
+ attempt.settled = true;
3009
+ controller.abort(error);
3010
+ reject(error);
3011
+ },
3012
+ rejectAfter(error, until) {
3013
+ if (attempt.settled) return;
3014
+ attempt.settled = true;
3015
+ controller.abort(error);
3016
+ until.finally(() => reject(error));
3017
+ }
3018
+ };
3019
+ return attempt;
3020
+ }
3021
+ /**
3022
+ * Voice deliveries are dropped while more than this many deliveries of any
3023
+ * kind are waiting: audio is fifty packets a second per speaker, and a
3024
+ * listener that cannot keep up would otherwise queue it without bound.
3025
+ */
3026
+ var MAX_PENDING_DELIVERIES = 256;
3027
+ /** How often dropped voice is reported, at most. */
3028
+ var VOICE_DROP_WARN_INTERVAL_MS = 1e4;
3029
+ /** The only commands handled before crypto init completes. */
3030
+ var PRE_INIT_COMMANDS = /* @__PURE__ */ new Set([
3031
+ "clientinitiv",
3032
+ "initivexpand2",
3033
+ "error"
3034
+ ]);
3035
+ /**
3036
+ * A TeamSpeak client over the voice protocol.
3037
+ *
3038
+ * Listeners run one at a time, in the order their events arrived, after the
3039
+ * packet that carried them has been handled, so a listener may call the client
3040
+ * and await an answer.
3041
+ */
3042
+ var Client = class {
3043
+ static {}
3044
+ #crypt;
3045
+ #handler;
3046
+ #logger;
3047
+ #nickname;
3048
+ #clid = 0;
3049
+ /**
3050
+ * What the server reported about itself in initserver, including the UID
3051
+ * hash it uses: TS3 hashes the public key with SHA-1 and TS6 with SHA-256.
3052
+ */
3053
+ #capabilities = unknownServerCapabilities();
3054
+ #internals;
3055
+ #identity;
3056
+ #addr;
3057
+ #resolver;
3058
+ #clientInitOptions;
3059
+ #socketFactory;
3060
+ #fileTransferSocketFactory;
3061
+ /** Undefined when the client does not reconnect. */
3062
+ #reconnectOptions;
3063
+ /**
3064
+ * Set while a reconnect attempt is in flight, so overlapping failures do not
3065
+ * start competing loops.
3066
+ */
3067
+ #reconnecting = false;
3068
+ /**
3069
+ * Why the last established session dropped: a reconnect loop whose own
3070
+ * attempt connected and then dropped before the loop resumed retries for it.
3071
+ */
3072
+ #lastDrop;
3073
+ /** Set by an explicit disconnect(), which must never be undone by a retry. */
3074
+ #stopped = false;
3075
+ /**
3076
+ * The goodbye of a disconnect() still in progress, which a connect() waits
3077
+ * for: tearing the old session down after a new one has started would end
3078
+ * the new one instead.
3079
+ */
3080
+ #leaving;
3081
+ /** How many times disconnect() was called, so a waiting connect() sees one. */
3082
+ #disconnects = 0;
3083
+ /**
3084
+ * Settles once the last queued `disconnected` has been delivered. A
3085
+ * connect() waits for it, so the end of one session is never heard during
3086
+ * the next: not by a listener added for the new session, and not by an
3087
+ * events() iterator started for it.
3088
+ */
3089
+ #disconnectedDelivered;
3090
+ /** Cuts short the reconnect loop's wait, on disconnect(). */
3091
+ #stopReconnecting;
3092
+ #status = ClientStatus.Disconnected;
3093
+ #throttle = new CommandThrottle();
3094
+ #cmdTrack = new CommandTracker(() => this.#clid);
3095
+ #clients = /* @__PURE__ */ new Map();
3096
+ #attempt = newConnectAttempt();
3097
+ #listeners = /* @__PURE__ */ new Map();
3098
+ /** Deliveries waiting for their turn; see #emit. */
3099
+ #queue = [];
3100
+ #draining = false;
3101
+ #voiceDropped = 0;
3102
+ #voiceDropWarnedAt = -Infinity;
3103
+ #eventMiddlewares;
3104
+ #send;
3105
+ #eventChain;
3106
+ constructor(identity, addr, nickname, options = {}) {
3107
+ this.#identity = identity;
3108
+ this.#addr = addr;
3109
+ this.#nickname = nickname;
3110
+ this.#logger = options.logger ?? noopLogger;
3111
+ this.#resolver = options.resolver ?? new Resolver({ resolveNicknames: options.resolveNicknames });
3112
+ this.#clientInitOptions = normalizeClientInitOptions(options);
3113
+ this.#socketFactory = options.socketFactory;
3114
+ this.#fileTransferSocketFactory = options.fileTransferSocketFactory;
3115
+ this.#reconnectOptions = options.reconnect;
3116
+ this.#crypt = new Crypt(identity);
3117
+ this.#handler = this.#newHandler();
3118
+ const self = this;
3119
+ this.#internals = {
3120
+ get crypt() {
3121
+ return self.#crypt;
3122
+ },
3123
+ get handler() {
3124
+ return self.#handler;
3125
+ },
3126
+ get logger() {
3127
+ return self.#logger;
3128
+ },
3129
+ get initOptions() {
3130
+ return self.#clientInitOptions;
3131
+ },
3132
+ get nickname() {
3133
+ return self.#nickname;
3134
+ },
3135
+ set nickname(value) {
3136
+ self.#nickname = value;
3137
+ },
3138
+ get clid() {
3139
+ return self.#clid;
3140
+ },
3141
+ set clid(value) {
3142
+ self.#clid = value;
3143
+ },
3144
+ get capabilities() {
3145
+ return self.#capabilities;
3146
+ },
3147
+ set capabilities(value) {
3148
+ self.#capabilities = value;
3149
+ },
3150
+ markConnected: () => this.#markConnected(),
3151
+ execNoWait: (command) => this.#execNoWait(command, void 0, true),
3152
+ handlePacket: (packet) => this.#handlePacket(packet)
3153
+ };
3154
+ const cmdMiddlewares = options.commandMiddleware ?? [];
3155
+ this.#eventMiddlewares = options.eventMiddleware ?? [];
3156
+ this.#send = buildCommandChain(cmdMiddlewares, (request, commandOptions = {}) => this.#exec(encodeRequest(request, prepareClientPassword), commandOptions, false));
3157
+ this.#eventChain = buildEventChain([...this.#eventMiddlewares], (event) => this.#deliver(event.name, event.payload));
3158
+ }
3159
+ /** Where the client is in its connection's life. */
3160
+ get status() {
3161
+ return this.#status;
3162
+ }
3163
+ /**
3164
+ * Connect to the server. Resolves once the handshake completes.
3165
+ *
3166
+ * An address that cannot be one — no host, a port outside 1–65535 — rejects
3167
+ * with an InvalidAddressError at once, before anything starts. A host name
3168
+ * that does not resolve fails the attempt with a TeamspeakError whose
3169
+ * `cause` is the lookup's error.
3170
+ *
3171
+ * A failed attempt rejects with its cause, such as the server's error for a
3172
+ * rejected password. Aborting `signal` abandons the attempt and rejects with
3173
+ * the signal's reason. The signal bounds only the connecting: once this
3174
+ * resolves, the connection lasts until `disconnect()` or a drop.
3175
+ * `connect()` does not retry; the `reconnect` option only retries a
3176
+ * connection that drops after it was established.
3177
+ *
3178
+ * Called while a `disconnect()` is still saying goodbye, or while an earlier
3179
+ * session's `disconnected` is still waiting to be delivered, this waits for
3180
+ * that first, so the old session is closed, and its `disconnected`
3181
+ * delivered, before the new one starts: no listener or `events()` iterator
3182
+ * of the new session hears the old one end. A `disconnect()` called during
3183
+ * that wait wins: this then rejects with a NotConnectedError.
3184
+ *
3185
+ * `connected` is delivered after this resolves, so a listener added right
3186
+ * after `await client.connect()` still hears it.
3187
+ */
3188
+ async connect(options = {}) {
3189
+ const { signal } = options;
3190
+ signal?.throwIfAborted();
3191
+ const disconnects = this.#disconnects;
3192
+ if (this.#leaving !== void 0) await abortable(this.#leaving, signal);
3193
+ while (this.#disconnectedDelivered !== void 0) await abortable(this.#disconnectedDelivered, signal);
3194
+ if (this.#disconnects !== disconnects) throw new NotConnectedError();
3195
+ if (this.#status !== ClientStatus.Disconnected) throw new AlreadyConnectedError();
3196
+ parseConnectAddress(this.#addr);
3197
+ this.#stopped = false;
3198
+ this.#resetForConnect();
3199
+ this.#status = ClientStatus.Connecting;
3200
+ const attempt = this.#attempt;
3201
+ const onAbort = () => this.#failConnection(abortReason(signal));
3202
+ signal?.addEventListener("abort", onAbort, { once: true });
3203
+ try {
3204
+ try {
3205
+ const targetAddr = await this.#resolveAddr(attempt.signal);
3206
+ if (!attempt.settled) {
3207
+ this.#logger.info("connecting to server", { address: targetAddr });
3208
+ await this.#handler.connect(targetAddr, attempt.signal);
3209
+ }
3210
+ } catch (error) {
3211
+ this.#failConnection(error);
3212
+ }
3213
+ await attempt.promise;
3214
+ } finally {
3215
+ signal?.removeEventListener("abort", onAbort);
3216
+ }
3217
+ }
3218
+ /**
3219
+ * Leave the server and close the connection.
3220
+ *
3221
+ * Says goodbye first, waiting for the server's acknowledgement for a second
3222
+ * at most, or less if `signal` aborts sooner. The connection is closed
3223
+ * either way. A `connect()` still waiting for its handshake rejects with a
3224
+ * NotConnectedError, and so do the commands still waiting for an answer.
3225
+ * Resolves once `disconnected` has been delivered to the listeners, so one
3226
+ * added after `await client.disconnect()` does not hear it. Does nothing on
3227
+ * a client that is not connected; called again while an earlier call is
3228
+ * still saying goodbye, it waits for that one.
3229
+ */
3230
+ async disconnect(options = {}) {
3231
+ this.#stopped = true;
3232
+ this.#disconnects++;
3233
+ this.#stopReconnecting?.abort();
3234
+ if (this.#leaving !== void 0) return this.#leaving;
3235
+ if (this.#status === ClientStatus.Disconnected) return this.#disconnectedDelivered;
3236
+ const wasConnected = this.#status === ClientStatus.Connected;
3237
+ this.#status = ClientStatus.Disconnected;
3238
+ let left;
3239
+ this.#attempt.rejectAfter(new NotConnectedError(), new Promise((r) => left = r));
3240
+ const leaving = this.#leave(wasConnected, options.signal).finally(left);
3241
+ this.#leaving = leaving;
3242
+ try {
3243
+ await leaving;
3244
+ } finally {
3245
+ this.#leaving = void 0;
3246
+ }
3247
+ }
3248
+ async #leave(wasConnected, signal) {
3249
+ this.#logger.info("disconnecting from server");
3250
+ if (wasConnected) try {
3251
+ await this.#exec("clientdisconnect reasonmsg=Shutdown", {
3252
+ signal,
3253
+ timeout: DISCONNECT_TIMEOUT_MS
3254
+ }, true);
3255
+ } catch {}
3256
+ this.#handler.close();
3257
+ this.#cmdTrack.reset(new NotConnectedError());
3258
+ await this.#announceDisconnected(void 0);
3259
+ }
3260
+ /** `await using client = …` disconnects when the scope ends. */
3261
+ [Symbol.asyncDispose]() {
3262
+ return this.disconnect();
3263
+ }
3264
+ /**
3265
+ * Send a raw command line and return the rows that answer it: the escape
3266
+ * hatch for a command the typed functions do not cover. The line is sent as
3267
+ * given, so its values must already be escaped and any channel password
3268
+ * already hashed.
3269
+ *
3270
+ * Rejects with a NotConnectedError unless the client is connected: while it
3271
+ * is still connecting, the session's keys may not exist yet.
3272
+ */
3273
+ exec(command, options = {}) {
3274
+ return this.#exec(command, options, false);
3275
+ }
3276
+ /**
3277
+ * Send a raw command line without waiting for an answer, for the few
3278
+ * commands the server answers with nothing at all.
3279
+ *
3280
+ * Commands are paced by a flood throttle; aborting `signal` while this waits
3281
+ * for its turn rejects with the signal's reason and sends nothing. Rejects
3282
+ * with a NotConnectedError unless the client is connected.
3283
+ */
3284
+ execNoWait(command, options = {}) {
3285
+ return this.#execNoWait(command, options.signal, false);
3286
+ }
3287
+ /**
3288
+ * Whether a command may go out now. The handshake's own commands (`internal`)
3289
+ * need only an open socket; everything else needs a connected session, so
3290
+ * nothing a caller sends can go out under the public dummy key, or after
3291
+ * disconnect() has begun saying goodbye.
3292
+ */
3293
+ #canSend(internal) {
3294
+ return this.#handler.isOpen && (internal || this.#status === ClientStatus.Connected);
3295
+ }
3296
+ async #execNoWait(command, signal, internal) {
3297
+ signal?.throwIfAborted();
3298
+ if (!this.#canSend(internal)) throw new NotConnectedError();
3299
+ await this.#throttle.wait(signal);
3300
+ if (!this.#canSend(internal)) throw new NotConnectedError();
3301
+ this.#sendLine(command);
3302
+ }
3303
+ /**
3304
+ * Send a typed command; this is the CommandExecutor the typed commands run on.
3305
+ *
3306
+ * The request passes through the `commandMiddleware` first. Channel
3307
+ * passwords are then hashed, because that is this protocol's rule: the voice
3308
+ * protocol expects `base64(sha1(password))`, where ServerQuery takes the
3309
+ * password itself.
3310
+ */
3311
+ send(request, options = {}) {
3312
+ return this.#send(request, options);
3313
+ }
3314
+ #sendLine(line) {
3315
+ this.#handler.sendPacket(PacketType.Command, Buffer.from(line), 0);
3316
+ }
3317
+ #exec(command, options, internal) {
3318
+ if (!commandChannel.hasSubscribers) return this.#execUntraced(command, options, internal);
3319
+ const trace = {
3320
+ command: parseCommand(command)?.name ?? "",
3321
+ client: this
3322
+ };
3323
+ return commandChannel.tracePromise(() => this.#execUntraced(command, options, internal), trace);
3324
+ }
3325
+ async #execUntraced(command, options, internal) {
3326
+ const { signal, timeout = DEFAULT_COMMAND_TIMEOUT_MS } = options;
3327
+ signal?.throwIfAborted();
3328
+ if (!this.#canSend(internal)) throw new NotConnectedError();
3329
+ const parsed = parseCommand(command);
3330
+ const spec = responseSpec(parsed?.name ?? "");
3331
+ const key = spec?.correlated?.key;
3332
+ const correlation = key === void 0 ? void 0 : parsed?.params[key];
3333
+ const [rc, answered] = this.#cmdTrack.register(spec, correlation);
3334
+ let timer;
3335
+ let onAbort;
3336
+ const givenUp = new Promise((_, reject) => {
3337
+ timer = setTimeout(() => reject(new CommandTimeoutError(parsed?.name ?? "")), timeout);
3338
+ onAbort = () => reject(abortReason(signal));
3339
+ signal?.addEventListener("abort", onAbort, { once: true });
3340
+ });
3341
+ givenUp.catch(() => {});
3342
+ const throttleAbort = new AbortController();
3343
+ try {
3344
+ const withRc = appendReturnCode(command, rc);
3345
+ await Promise.race([this.#throttle.wait(throttleAbort.signal), givenUp]);
3346
+ if (!this.#canSend(internal)) throw new NotConnectedError();
3347
+ this.#sendLine(withRc);
3348
+ const result = await Promise.race([answered, givenUp]);
3349
+ if (result.err) throw result.err;
3350
+ return result.data;
3351
+ } finally {
3352
+ throttleAbort.abort();
3353
+ clearTimeout(timer);
3354
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
3355
+ this.#cmdTrack.unregister(rc);
3356
+ }
3357
+ }
3358
+ /**
3359
+ * Listen for an event. Returns a function that removes the listener.
3360
+ *
3361
+ * A listener that throws does not stop the others: the rest of the event's
3362
+ * listeners, and the events after it, are still delivered, and the exception
3363
+ * is rethrown on its own, from a microtask, so it surfaces as an
3364
+ * `uncaughtException` just as an EventEmitter listener's would. An async
3365
+ * listener's rejection is not caught, and surfaces as an unhandled
3366
+ * rejection.
3367
+ */
3368
+ on(event, listener) {
3369
+ const subscription = {
3370
+ listener,
3371
+ removed: false
3372
+ };
3373
+ this.#listeners.set(event, [...this.#listeners.get(event) ?? [], subscription]);
3374
+ const unsubscribe = () => {
3375
+ if (subscription.removed) return;
3376
+ subscription.removed = true;
3377
+ const list = this.#listeners.get(event) ?? [];
3378
+ this.#listeners.set(event, list.filter((s) => s !== subscription));
3379
+ };
3380
+ return Object.assign(unsubscribe, { [Symbol.dispose]: unsubscribe });
3381
+ }
3382
+ /** Listen for the next occurrence of an event only. */
3383
+ once(event, listener) {
3384
+ const unsubscribe = this.on(event, ((payload) => {
3385
+ unsubscribe();
3386
+ listener(payload);
3387
+ }));
3388
+ return unsubscribe;
3389
+ }
3390
+ /**
3391
+ * An event as an async iterator, for `for await`:
3392
+ *
3393
+ * ```ts
3394
+ * for await (const message of client.events("textMessage", { signal })) { … }
3395
+ * ```
3396
+ *
3397
+ * Events that arrive while the loop body runs are buffered, so none is
3398
+ * missed — except voice, of which at most 256 frames are buffered; the rest
3399
+ * are dropped, as for a listener that falls behind. Iteration ends when
3400
+ * `signal` aborts, when the loop exits early (`break`, `return`, a throw),
3401
+ * and — for every event but `disconnected` — when the client disconnects.
3402
+ * An abort stops listening at once and discards whatever is buffered.
3403
+ */
3404
+ events(event, options = {}) {
3405
+ const { signal } = options;
3406
+ const buffered = [];
3407
+ let wake;
3408
+ let ended = false;
3409
+ const stopListening = this.on(event, ((payload) => {
3410
+ if (ended) return;
3411
+ if (event === "voiceData" && buffered.length >= MAX_PENDING_DELIVERIES) {
3412
+ this.#dropVoice();
3413
+ return;
3414
+ }
3415
+ buffered.push(payload);
3416
+ wake?.();
3417
+ }));
3418
+ const stopOnDisconnect = event === "disconnected" ? void 0 : this.on("disconnected", () => {
3419
+ ended = true;
3420
+ stopListening();
3421
+ wake?.();
3422
+ });
3423
+ const cleanup = () => {
3424
+ ended = true;
3425
+ stopListening();
3426
+ stopOnDisconnect?.();
3427
+ signal?.removeEventListener("abort", abort);
3428
+ wake?.();
3429
+ };
3430
+ const abort = () => {
3431
+ buffered.length = 0;
3432
+ cleanup();
3433
+ };
3434
+ if (signal?.aborted) abort();
3435
+ else signal?.addEventListener("abort", abort, { once: true });
3436
+ const iterator = {
3437
+ async next() {
3438
+ while (buffered.length === 0 && !ended) {
3439
+ await new Promise((resolve) => wake = resolve);
3440
+ wake = void 0;
3441
+ }
3442
+ if (buffered.length > 0) return {
3443
+ value: buffered.shift(),
3444
+ done: false
3445
+ };
3446
+ cleanup();
3447
+ return {
3448
+ value: void 0,
3449
+ done: true
3450
+ };
3451
+ },
3452
+ async return() {
3453
+ buffered.length = 0;
3454
+ cleanup();
3455
+ return {
3456
+ value: void 0,
3457
+ done: true
3458
+ };
3459
+ },
3460
+ [Symbol.asyncIterator]() {
3461
+ return iterator;
3462
+ }
3463
+ };
3464
+ return iterator;
3465
+ }
3466
+ /** This client's id on the server, assigned in the handshake; 0 before it. */
3467
+ get clientID() {
3468
+ return asClientID(this.#clid);
3469
+ }
3470
+ /**
3471
+ * What the connected server is: version, generation, and the uid hash it
3472
+ * uses. Read from `initserver`; before connecting the family is `unknown`
3473
+ * and the uid hash SHA-1.
3474
+ */
3475
+ get serverCapabilities() {
3476
+ return this.#capabilities;
3477
+ }
3478
+ /**
3479
+ * Our own `client_unique_identifier`, hashed with the algorithm the connected
3480
+ * server uses.
3481
+ *
3482
+ * TS3 uses SHA-1 and TS6 SHA-256, so this reads the algorithm learned during
3483
+ * initserver rather than assuming one. Comparing a hard-coded SHA-1 UID to a
3484
+ * TS6 server silently never matches.
3485
+ */
3486
+ get uid() {
3487
+ return this.#identity.uid(this.#capabilities.uidAlgorithm);
3488
+ }
3489
+ /** The channel this client is in, as it last learned; 0 before connecting. */
3490
+ get channelID() {
3491
+ return this.#clients.get(this.#clid)?.channelID ?? asChannelID(0n);
3492
+ }
3493
+ /**
3494
+ * Send Opus voice directly to specific clients, bypassing channel routing.
3495
+ * Throws a NotConnectedError unless the client is connected.
3496
+ */
3497
+ sendWhisper(data, targetClientIDs, codec) {
3498
+ if (!this.#canSend(false)) throw new NotConnectedError();
3499
+ this.#handler.sendWhisperPacket(data, [...targetClientIDs], codec);
3500
+ }
3501
+ /**
3502
+ * Send one encoded audio frame to the channel this client is in. Throws a
3503
+ * NotConnectedError unless the client is connected.
3504
+ */
3505
+ sendVoice(data, codec) {
3506
+ if (!this.#canSend(false)) throw new NotConnectedError();
3507
+ this.#handler.sendVoicePacket(data, codec);
3508
+ }
3509
+ /**
3510
+ * Upload `data` over a negotiated transfer, through the client's
3511
+ * `fileTransferSocketFactory` if it has one.
3512
+ */
3513
+ uploadFileData(host, info, data, options = {}) {
3514
+ return uploadFileData(host, info, data, {
3515
+ ...options,
3516
+ socketFactory: this.#fileTransferSocketFactory
3517
+ });
3518
+ }
3519
+ /**
3520
+ * Download a negotiated transfer into `dest`, through the client's
3521
+ * `fileTransferSocketFactory` if it has one.
3522
+ */
3523
+ downloadFileData(host, info, dest, options = {}) {
3524
+ return downloadFileData(host, info, dest, {
3525
+ ...options,
3526
+ socketFactory: this.#fileTransferSocketFactory
3527
+ });
3528
+ }
3529
+ #markConnected() {
3530
+ if (this.#status !== ClientStatus.Connecting || this.#attempt.settled) return;
3531
+ this.#status = ClientStatus.Connected;
3532
+ this.#attempt.resolve();
3533
+ this.#emit("connected", void 0);
3534
+ }
3535
+ #newHandler() {
3536
+ const handler = new PacketHandler(this.#crypt, this.#logger, this.#socketFactory);
3537
+ handler.onPacket = (p) => {
3538
+ if (handler === this.#handler) this.#handlePacket(p);
3539
+ };
3540
+ handler.onClosed = (err) => {
3541
+ if (handler === this.#handler) this.#handleConnectionClosed(err);
3542
+ };
3543
+ handler.onReceiveError = (error) => {
3544
+ if (handler === this.#handler) this.#handleReceiveError(error);
3545
+ };
3546
+ return handler;
3547
+ }
3548
+ #resetForConnect() {
3549
+ this.#handler.close();
3550
+ this.#crypt = new Crypt(this.#identity);
3551
+ this.#handler = this.#newHandler();
3552
+ this.#attempt = newConnectAttempt();
3553
+ this.#cmdTrack.reset(new NotConnectedError());
3554
+ this.#clients.clear();
3555
+ this.#clid = 0;
3556
+ this.#capabilities = unknownServerCapabilities();
3557
+ }
3558
+ async #resolveAddr(signal) {
3559
+ const { host, port } = splitHostPort(this.#addr);
3560
+ const fallback = joinHostPort(host, port);
3561
+ const budget = new AbortController();
3562
+ const timer = setTimeout(() => budget.abort(new TeamspeakError("address resolution took too long")), RESOLVE_BUDGET_MS);
3563
+ const lookup = AbortSignal.any([signal, budget.signal]);
3564
+ try {
3565
+ return (await abortable(this.#resolver.resolve(this.#addr, { signal: lookup }), lookup))[0]?.addr ?? fallback;
3566
+ } catch (error) {
3567
+ signal.throwIfAborted();
3568
+ this.#logger.warn("address resolution failed, falling back to direct", { error: String(budget.signal.aborted ? budget.signal.reason : error) });
3569
+ return fallback;
3570
+ } finally {
3571
+ clearTimeout(timer);
3572
+ }
3573
+ }
3574
+ /**
3575
+ * A datagram from the server could not be handled. During the handshake that
3576
+ * is the connection failing, reported as a CryptoInitError with the cause;
3577
+ * once connected, one bad packet is not worth the session, so it is dropped.
3578
+ */
3579
+ #handleReceiveError(error) {
3580
+ if (this.#status !== ClientStatus.Connected) {
3581
+ this.#failConnection(error instanceof CryptoInitError ? error : new CryptoInitError("the handshake failed on a malformed packet", { cause: error }));
3582
+ return;
3583
+ }
3584
+ this.#logger.debug("dropped a malformed packet", { error: String(error) });
3585
+ }
3586
+ #handlePacket(p) {
3587
+ const pType = p.typeFlagged & 15;
3588
+ if (pType === 8) {
3589
+ if (this.#crypt.cryptoInitComplete) return;
3590
+ const response = processInit1(this.#crypt, p.data);
3591
+ if (response) this.#handler.sendPacket(PacketType.Init1, response, 0);
3592
+ return;
3593
+ }
3594
+ if ((pType === 0 || pType === 1) && p.data.length > 5) {
3595
+ this.#handleVoicePacket(p.data);
3596
+ return;
3597
+ }
3598
+ if ((pType === 2 || pType === 3) && p.data.length > 0) this.#handleCommandLines(Buffer.from(p.data).toString("utf8"));
3599
+ }
3600
+ /**
3601
+ * Parse an incoming S2C voice packet.
3602
+ * Format: [VId: u16] [CId: u16] [Codec: u8] [Data: var]
3603
+ */
3604
+ #handleVoicePacket(payload) {
3605
+ if (!this.#listeners.get("voiceData")?.length && this.#eventMiddlewares.length === 0) return;
3606
+ if (this.#queue.length > MAX_PENDING_DELIVERIES) {
3607
+ this.#dropVoice();
3608
+ return;
3609
+ }
3610
+ const clientId = new DataView(payload.buffer, payload.byteOffset, payload.byteLength).getUint16(2, false);
3611
+ if (clientId === this.#clid) return;
3612
+ const data = payload.slice(5);
3613
+ this.#emitServerEvent({
3614
+ name: "voiceData",
3615
+ payload: {
3616
+ clientID: asClientID(clientId),
3617
+ codec: payload[4],
3618
+ data
3619
+ }
3620
+ });
3621
+ }
3622
+ #handleCommandLines(s) {
3623
+ if (!s) return;
3624
+ const lines = s.split("\0").join("\n").split("\n");
3625
+ for (const line of lines) {
3626
+ const trimmed = line.replace(/\r$/, "");
3627
+ if (!trimmed) continue;
3628
+ for (const row of splitCommandRows(trimmed)) this.#handleCommandStr(row);
3629
+ }
3630
+ }
3631
+ #handleCommandStr(s) {
3632
+ const cmd = parseCommand(s);
3633
+ if (!cmd) return;
3634
+ if (!this.#crypt.cryptoInitComplete && !PRE_INIT_COMMANDS.has(cmd.name)) {
3635
+ this.#logger.debug("dropped a command that arrived before crypto init", { command: cmd.name });
3636
+ return;
3637
+ }
3638
+ if (packetChannel.hasSubscribers) packetChannel.publish({
3639
+ command: cmd.name,
3640
+ client: this
3641
+ });
3642
+ if (cmd.name === "") {
3643
+ if (Object.keys(cmd.params).length > 0) this.#cmdTrack.buffer(cmd.params);
3644
+ return;
3645
+ }
3646
+ if (cmd.name === "channellistfinished") {
3647
+ this.#cmdTrack.signalWelcomeComplete();
3648
+ return;
3649
+ }
3650
+ if (cmd.name.startsWith("notify")) {
3651
+ if (this.#cmdTrack.expects(cmd.name)) {
3652
+ this.#cmdTrack.buffer(cmd.params, cmd.name);
3653
+ this.#cmdTrack.offerCorrelated(cmd.name, cmd.params);
3654
+ }
3655
+ const result = handleNotification(cmd, this.#clid, this.#clients, this.#nickname);
3656
+ this.#processNotificationResult(result);
3657
+ if (this.#listeners.get("notification")?.length || this.#eventMiddlewares.length > 0) this.#emitServerEvent({
3658
+ name: "notification",
3659
+ payload: {
3660
+ name: cmd.name,
3661
+ params: { ...cmd.params }
3662
+ }
3663
+ });
3664
+ return;
3665
+ }
3666
+ switch (cmd.name) {
3667
+ case "clientinitiv":
3668
+ if (!this.#crypt.cryptoInitComplete) handleHandshakeInitIV(this.#internals, cmd.params);
3669
+ break;
3670
+ case "initivexpand2":
3671
+ if (!this.#crypt.cryptoInitComplete) handleHandshakeExpand2(this.#internals, cmd.params);
3672
+ break;
3673
+ case "initserver":
3674
+ if (this.#status === ClientStatus.Connecting && this.#crypt.cryptoInitComplete) handleInitServer(this.#internals, cmd.params);
3675
+ break;
3676
+ case "error":
3677
+ this.#handleError(cmd.params);
3678
+ break;
3679
+ default: this.#cmdTrack.buffer(cmd.params, cmd.name);
3680
+ }
3681
+ }
3682
+ #handleError(params) {
3683
+ const { err, rc } = parseServerError(params);
3684
+ if (rc !== null) this.#cmdTrack.resolve(rc, err);
3685
+ else this.#cmdTrack.discardBuffer();
3686
+ if (err && this.#status !== ClientStatus.Connected) {
3687
+ this.#failConnection(err);
3688
+ return;
3689
+ }
3690
+ if (err?.code === ErrorCode.ConnectFailedBanned) {
3691
+ this.#logger.warn("banned by the server, closing the connection");
3692
+ setImmediate(() => this.disconnect().catch(() => {}));
3693
+ }
3694
+ }
3695
+ #processNotificationResult(result) {
3696
+ switch (result.kind) {
3697
+ case "clientEnter": {
3698
+ const info = result.info;
3699
+ if (info.id === this.#clid) this.#nickname = info.nickname;
3700
+ else if (this.#clid === 0 && isAutoNicknameMatch(this.#nickname, info.nickname)) {
3701
+ this.#clid = info.id;
3702
+ this.#handler.setClientID(info.id);
3703
+ this.#nickname = info.nickname;
3704
+ }
3705
+ this.#emitServerEvent({
3706
+ name: "clientEnter",
3707
+ payload: info
3708
+ });
3709
+ break;
3710
+ }
3711
+ case "clientLeave": {
3712
+ const event = result.event;
3713
+ this.#emitServerEvent({
3714
+ name: "clientLeave",
3715
+ payload: event
3716
+ });
3717
+ if (!result.isSelf) break;
3718
+ if (event.reason === Reason.KickServer || event.reason === Reason.KickServerBan) this.#emit("kicked", kickedEvent(event));
3719
+ this.#removedByServer(new RemovedError(event.reason, event.reasonMessage, event.invokerName));
3720
+ break;
3721
+ }
3722
+ case "clientMoved":
3723
+ this.#emitServerEvent({
3724
+ name: "clientMoved",
3725
+ payload: result.event
3726
+ });
3727
+ if (result.event.clientID === this.#clid && result.event.reason === Reason.KickChannel) this.#emit("kicked", kickedEvent({
3728
+ ...result.event,
3729
+ reasonMessage: result.reasonMessage
3730
+ }));
3731
+ break;
3732
+ case "textMessage":
3733
+ this.#emitServerEvent({
3734
+ name: "textMessage",
3735
+ payload: result.message
3736
+ });
3737
+ break;
3738
+ case "poked": this.#emitServerEvent({
3739
+ name: "poked",
3740
+ payload: result.event
3741
+ });
3742
+ }
3743
+ }
3744
+ /**
3745
+ * End a session the server has ended. The server stops answering a client it
3746
+ * has removed, so waiting for the transport to notice reported the end a
3747
+ * minute late, without its cause, and with the client claiming to be
3748
+ * connected meanwhile.
3749
+ */
3750
+ #removedByServer(cause) {
3751
+ this.#handleConnectionClosed(cause);
3752
+ this.#handler.close();
3753
+ }
3754
+ /**
3755
+ * Count a dropped voice delivery, and report the drops now and then: at most
3756
+ * once every ten seconds, with how many were dropped since the last report.
3757
+ */
3758
+ #dropVoice() {
3759
+ this.#voiceDropped++;
3760
+ const now = Date.now();
3761
+ if (now - this.#voiceDropWarnedAt < VOICE_DROP_WARN_INTERVAL_MS) return;
3762
+ this.#voiceDropWarnedAt = now;
3763
+ this.#logger.warn("voice dropped: listeners are not keeping up", { dropped: this.#voiceDropped });
3764
+ this.#voiceDropped = 0;
3765
+ }
3766
+ /** Queue a server event, through the event middleware. */
3767
+ #emitServerEvent(event) {
3768
+ this.#enqueue(() => this.#eventChain(event));
3769
+ }
3770
+ /**
3771
+ * Queue `disconnected`; the promise settles once it has been delivered, and
3772
+ * a connect() waits for it until then.
3773
+ */
3774
+ #announceDisconnected(cause) {
3775
+ const delivered = new Promise((resolve) => this.#enqueue(() => {
3776
+ try {
3777
+ this.#deliver("disconnected", cause);
3778
+ } finally {
3779
+ resolve();
3780
+ }
3781
+ }));
3782
+ this.#disconnectedDelivered = delivered;
3783
+ delivered.then(() => {
3784
+ if (this.#disconnectedDelivered === delivered) this.#disconnectedDelivered = void 0;
3785
+ });
3786
+ return delivered;
3787
+ }
3788
+ /** Queue a lifecycle event, which middleware does not see. */
3789
+ #emit(event, payload) {
3790
+ this.#enqueue(() => this.#deliver(event, payload));
3791
+ }
3792
+ #deliver(event, payload) {
3793
+ for (const subscription of this.#listeners.get(event) ?? []) {
3794
+ if (subscription.removed) continue;
3795
+ try {
3796
+ subscription.listener(payload);
3797
+ } catch (error) {
3798
+ queueMicrotask(() => {
3799
+ throw error;
3800
+ });
3801
+ }
3802
+ }
3803
+ }
3804
+ /**
3805
+ * Run deliveries one at a time, in order, once the packet being handled is
3806
+ * done with. A listener that throws does so as an uncaught exception, as it
3807
+ * would from an EventEmitter, and the listeners and deliveries behind it
3808
+ * still run.
3809
+ */
3810
+ #enqueue(delivery) {
3811
+ this.#queue.push(delivery);
3812
+ if (this.#draining) return;
3813
+ this.#draining = true;
3814
+ setImmediate(this.#drain);
3815
+ }
3816
+ #drain = () => {
3817
+ try {
3818
+ while (this.#queue.length > 0) this.#queue.shift()();
3819
+ } finally {
3820
+ if (this.#queue.length > 0) setImmediate(this.#drain);
3821
+ else this.#draining = false;
3822
+ }
3823
+ };
3824
+ #handleConnectionClosed(err) {
3825
+ if (this.#status === ClientStatus.Disconnected) return;
3826
+ this.#cmdTrack.reset(new NotConnectedError(err ? { cause: err } : void 0));
3827
+ if (this.#status !== ClientStatus.Connected) {
3828
+ this.#failConnection(err ?? new TeamspeakError("connection closed before the TeamSpeak handshake completed"));
3829
+ return;
3830
+ }
3831
+ this.#status = ClientStatus.Disconnected;
3832
+ const cause = err ?? new TeamspeakError("connection closed");
3833
+ this.#lastDrop = cause;
3834
+ this.#announceDisconnected(cause);
3835
+ this.#scheduleReconnect(cause).catch((error) => {
3836
+ this.#logger.error("reconnecting failed", { error: String(error) });
3837
+ });
3838
+ }
3839
+ /**
3840
+ * Retry a dropped connection according to the reconnect policy.
3841
+ *
3842
+ * Never runs after an explicit disconnect, and never more than one at a time:
3843
+ * two failures in quick succession would otherwise start competing loops, and
3844
+ * a loop shorter than the server's antiflood window bans the IP rather than
3845
+ * reconnecting it. A drop while the loop runs is the loop's to retry: it
3846
+ * ends only once its attempt is still connected when it resumes.
3847
+ */
3848
+ async #scheduleReconnect(err) {
3849
+ const options = this.#reconnectOptions;
3850
+ if (options === void 0 || this.#stopped || this.#reconnecting) return;
3851
+ const policy = resolveBackoffPolicy(options);
3852
+ const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
3853
+ this.#reconnecting = true;
3854
+ const stop = new AbortController();
3855
+ this.#stopReconnecting = stop;
3856
+ try {
3857
+ for (let attempt = 1; policy.maxAttempts === 0 || attempt <= policy.maxAttempts; attempt++) {
3858
+ if (this.#stopped) return;
3859
+ let retry;
3860
+ try {
3861
+ retry = shouldRetry(err, attempt);
3862
+ } catch (error) {
3863
+ this.#logger.error("shouldRetry threw; not reconnecting", { error: String(error) });
3864
+ return;
3865
+ }
3866
+ if (!retry) {
3867
+ this.#logger.info("not retrying", {
3868
+ attempt,
3869
+ reason: err.message
3870
+ });
3871
+ return;
3872
+ }
3873
+ const delay = backoffDelay(policy, attempt);
3874
+ this.#logger.info("reconnecting", {
3875
+ attempt,
3876
+ delayMs: delay
3877
+ });
3878
+ await sleep(delay, stop.signal);
3879
+ if (this.#stopped) return;
3880
+ if (this.#status !== ClientStatus.Disconnected) {
3881
+ this.#logger.info("reconnect loop ended: connected by other means", { attempt });
3882
+ return;
3883
+ }
3884
+ try {
3885
+ await this.connect({ signal: AbortSignal.timeout(RECONNECT_ATTEMPT_TIMEOUT_MS) });
3886
+ if (this.status === ClientStatus.Connected) {
3887
+ this.#logger.info("reconnected", { attempt });
3888
+ return;
3889
+ }
3890
+ err = this.#lastDrop ?? new TeamspeakError("connection closed");
3891
+ } catch (retryErr) {
3892
+ if (retryErr instanceof AlreadyConnectedError) return;
3893
+ err = retryErr instanceof Error ? retryErr : new TeamspeakError(String(retryErr));
3894
+ }
3895
+ }
3896
+ this.#logger.warn("reconnect attempts exhausted", { attempts: policy.maxAttempts });
3897
+ } finally {
3898
+ this.#reconnecting = false;
3899
+ if (this.#stopReconnecting === stop) this.#stopReconnecting = void 0;
3900
+ }
3901
+ }
3902
+ /**
3903
+ * A connection attempt has failed for good: settle it with the cause, and
3904
+ * tear the connection down. Does nothing once the handshake has completed,
3905
+ * so it cannot end a live session.
3906
+ *
3907
+ * Without this, a handshake the server rejects (a wrong password, say, which
3908
+ * the server answers with an error instead of closing the socket) surfaces
3909
+ * only as the caller's own timeout, and the client stays in `Connecting`
3910
+ * forever without ever emitting `disconnected`.
3911
+ */
3912
+ #failConnection(error) {
3913
+ if (this.#status === ClientStatus.Connected) return;
3914
+ this.#attempt.reject(error);
3915
+ if (this.#status === ClientStatus.Disconnected) return;
3916
+ this.#status = ClientStatus.Disconnected;
3917
+ this.#handler.close();
3918
+ this.#announceDisconnected(error instanceof Error ? error : new TeamspeakError(String(error)));
3919
+ }
3920
+ };
3921
+ function kickedEvent(event) {
3922
+ return {
3923
+ reason: event.reason,
3924
+ reasonMessage: event.reasonMessage,
3925
+ invokerName: event.invokerName,
3926
+ invokerUID: event.invokerUID,
3927
+ invokerID: event.invokerID
3928
+ };
3929
+ }
3930
+ //#endregion
3931
+ //#region src/api/voice.ts
3932
+ /** The channel's description, which `listChannels` omits for bandwidth. */
3933
+ async function getChannelDescription(client, cid, options) {
3934
+ return (await client.send({
3935
+ command: "channelgetdescription",
3936
+ params: [["cid", cid]]
3937
+ }, options))[0]?.["channel_description"] ?? "";
3938
+ }
3939
+ /**
3940
+ * Subscribe to a channel: receive its clients' events and voice without
3941
+ * joining it.
3942
+ */
3943
+ async function subscribeChannel(client, cid, options) {
3944
+ await client.send({
3945
+ command: "channelsubscribe",
3946
+ params: [["cid", cid]]
3947
+ }, options);
3948
+ }
3949
+ /** Stop receiving a subscribed channel's events and voice. */
3950
+ async function unsubscribeChannel(client, cid, options) {
3951
+ await client.send({
3952
+ command: "channelunsubscribe",
3953
+ params: [["cid", cid]]
3954
+ }, options);
3955
+ }
3956
+ /** Subscribe to every channel on the server. */
3957
+ async function subscribeAllChannels(client, options) {
3958
+ await client.send({ command: "channelsubscribeall" }, options);
3959
+ }
3960
+ /** Unsubscribe from every channel on the server. */
3961
+ async function unsubscribeAllChannels(client, options) {
3962
+ await client.send({ command: "channelunsubscribeall" }, options);
3963
+ }
3964
+ //#endregion
3965
+ export { AlreadyConnectedError, Client, ClientStatus, ConnectionTimeoutError, CryptoInitError, FileTransferError, Identity, InvalidAddressError, InvalidIdentityError, RemovedError, backoffDelay, commandChannel, defaultBackoffPolicy, defaultShouldRetry, dialFileTransfer, downloadFileData, generateIdentity, generateIdentitySync, getChannelDescription, packetChannel, parseIdentity, subscribeAllChannels, subscribeChannel, uidFromPublicKey, unknownServerCapabilities, unsubscribeAllChannels, unsubscribeChannel, uploadFileData };
3966
+
3967
+ //# sourceMappingURL=index.js.map