@honeybbq/teamspeak-client 0.3.0 → 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 (178) 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/eax.d.ts.map +1 -1
  17. package/dist/crypto/identity.d.ts +87 -8
  18. package/dist/crypto/identity.d.ts.map +1 -1
  19. package/dist/crypto/index.d.ts +2 -1
  20. package/dist/crypto/index.d.ts.map +1 -1
  21. package/dist/diagnostics.d.ts +45 -0
  22. package/dist/diagnostics.d.ts.map +1 -0
  23. package/dist/discovery/resolver.d.ts +21 -3
  24. package/dist/discovery/resolver.d.ts.map +1 -1
  25. package/dist/errors.d.ts +70 -16
  26. package/dist/errors.d.ts.map +1 -1
  27. package/dist/events.d.ts +4 -5
  28. package/dist/events.d.ts.map +1 -1
  29. package/dist/handshake/crypt-handshake.d.ts +1 -0
  30. package/dist/handshake/crypt-handshake.d.ts.map +1 -1
  31. package/dist/handshake/crypt-init2.d.ts.map +1 -1
  32. package/dist/handshake/license.d.ts +7 -0
  33. package/dist/handshake/license.d.ts.map +1 -1
  34. package/dist/handshake.d.ts +14 -5
  35. package/dist/handshake.d.ts.map +1 -1
  36. package/dist/helpers.d.ts +0 -8
  37. package/dist/helpers.d.ts.map +1 -1
  38. package/dist/index.d.ts +20 -6
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3967 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/notifications.d.ts +7 -15
  43. package/dist/notifications.d.ts.map +1 -1
  44. package/dist/reconnect.d.ts +69 -0
  45. package/dist/reconnect.d.ts.map +1 -0
  46. package/dist/throttle.d.ts +9 -0
  47. package/dist/throttle.d.ts.map +1 -1
  48. package/dist/transfer.d.ts +27 -25
  49. package/dist/transfer.d.ts.map +1 -1
  50. package/dist/transport/generation-window.d.ts.map +1 -1
  51. package/dist/transport/handler.d.ts +37 -5
  52. package/dist/transport/handler.d.ts.map +1 -1
  53. package/dist/transport/quicklz.d.ts +19 -0
  54. package/dist/transport/quicklz.d.ts.map +1 -1
  55. package/dist/types.d.ts +277 -97
  56. package/dist/types.d.ts.map +1 -1
  57. package/package.json +21 -57
  58. package/src/abort.ts +24 -0
  59. package/src/address.ts +74 -0
  60. package/src/api/voice.ts +61 -0
  61. package/src/capabilities.ts +92 -0
  62. package/src/client.ts +1392 -0
  63. package/src/commands.ts +193 -0
  64. package/src/crypto/crypt-ops.ts +6 -0
  65. package/src/crypto/crypt.ts +274 -0
  66. package/src/crypto/eax.ts +152 -0
  67. package/src/crypto/identity.ts +484 -0
  68. package/src/crypto/index.ts +21 -0
  69. package/src/crypto/primitives.ts +141 -0
  70. package/src/diagnostics.ts +58 -0
  71. package/src/discovery/index.ts +1 -0
  72. package/src/discovery/resolver.ts +272 -0
  73. package/src/errors.ts +156 -0
  74. package/src/events.ts +28 -0
  75. package/src/handshake/crypt-handshake.ts +135 -0
  76. package/src/handshake/crypt-init2.ts +48 -0
  77. package/src/handshake/index.ts +3 -0
  78. package/src/handshake/license.test.ts +49 -0
  79. package/src/handshake/license.ts +208 -0
  80. package/src/handshake.ts +136 -0
  81. package/src/helpers.ts +10 -0
  82. package/src/index.ts +74 -0
  83. package/src/notifications.ts +136 -0
  84. package/src/reconnect.ts +141 -0
  85. package/src/throttle.ts +92 -0
  86. package/src/transfer.ts +127 -0
  87. package/src/transport/generation-window.ts +77 -0
  88. package/src/transport/handler.ts +914 -0
  89. package/src/transport/index.ts +14 -0
  90. package/src/transport/packet.ts +69 -0
  91. package/src/transport/quicklz.ts +209 -0
  92. package/src/types.ts +349 -0
  93. package/dist/address-BR-5unVo.cjs +0 -2
  94. package/dist/address-BR-5unVo.cjs.map +0 -1
  95. package/dist/address-BfmovVpp.js +0 -65
  96. package/dist/address-BfmovVpp.js.map +0 -1
  97. package/dist/address.test.d.ts +0 -2
  98. package/dist/address.test.d.ts.map +0 -1
  99. package/dist/api.d.ts +0 -17
  100. package/dist/api.d.ts.map +0 -1
  101. package/dist/api.test.d.ts +0 -2
  102. package/dist/api.test.d.ts.map +0 -1
  103. package/dist/command/command.d.ts +0 -12
  104. package/dist/command/command.d.ts.map +0 -1
  105. package/dist/command/command.test.d.ts +0 -2
  106. package/dist/command/command.test.d.ts.map +0 -1
  107. package/dist/command/index.cjs +0 -1
  108. package/dist/command/index.d.ts +0 -4
  109. package/dist/command/index.d.ts.map +0 -1
  110. package/dist/command/index.mjs +0 -3
  111. package/dist/command/parser.d.ts +0 -3
  112. package/dist/command/parser.d.ts.map +0 -1
  113. package/dist/command-BLe-C2L2.js +0 -74
  114. package/dist/command-BLe-C2L2.js.map +0 -1
  115. package/dist/command-DAK4pUBP.cjs +0 -4
  116. package/dist/command-DAK4pUBP.cjs.map +0 -1
  117. package/dist/crypt-handshake-CQkQreLx.cjs +0 -2
  118. package/dist/crypt-handshake-CQkQreLx.cjs.map +0 -1
  119. package/dist/crypt-handshake-DziOY4Lq.js +0 -55
  120. package/dist/crypt-handshake-DziOY4Lq.js.map +0 -1
  121. package/dist/crypt-init2-D70ha4LB.js +0 -158
  122. package/dist/crypt-init2-D70ha4LB.js.map +0 -1
  123. package/dist/crypt-init2-DparLekm.cjs +0 -2
  124. package/dist/crypt-init2-DparLekm.cjs.map +0 -1
  125. package/dist/crypto/eax.test.d.ts +0 -2
  126. package/dist/crypto/eax.test.d.ts.map +0 -1
  127. package/dist/crypto/identity.test.d.ts +0 -2
  128. package/dist/crypto/identity.test.d.ts.map +0 -1
  129. package/dist/crypto/index.cjs +0 -1
  130. package/dist/crypto/index.mjs +0 -3
  131. package/dist/crypto-Bg02m4ZE.js +0 -165
  132. package/dist/crypto-Bg02m4ZE.js.map +0 -1
  133. package/dist/crypto-CDlweUL3.cjs +0 -2
  134. package/dist/crypto-CDlweUL3.cjs.map +0 -1
  135. package/dist/discovery/index.cjs +0 -1
  136. package/dist/discovery/index.mjs +0 -2
  137. package/dist/handler-DI_abe9c.js +0 -483
  138. package/dist/handler-DI_abe9c.js.map +0 -1
  139. package/dist/handler-DuwoO5RC.cjs +0 -2
  140. package/dist/handler-DuwoO5RC.cjs.map +0 -1
  141. package/dist/handshake/index.cjs +0 -1
  142. package/dist/handshake/index.mjs +0 -3
  143. package/dist/handshake.test.d.ts +0 -2
  144. package/dist/handshake.test.d.ts.map +0 -1
  145. package/dist/helpers.test.d.ts +0 -2
  146. package/dist/helpers.test.d.ts.map +0 -1
  147. package/dist/index.cjs +0 -4
  148. package/dist/index.cjs.map +0 -1
  149. package/dist/index.mjs +0 -796
  150. package/dist/index.mjs.map +0 -1
  151. package/dist/integration.test.d.ts +0 -12
  152. package/dist/integration.test.d.ts.map +0 -1
  153. package/dist/notifications.test.d.ts +0 -2
  154. package/dist/notifications.test.d.ts.map +0 -1
  155. package/dist/parser-CL33R9tr.cjs +0 -2
  156. package/dist/parser-CL33R9tr.cjs.map +0 -1
  157. package/dist/parser-DZMv3Jm-.js +0 -33
  158. package/dist/parser-DZMv3Jm-.js.map +0 -1
  159. package/dist/primitives-cNNAeFCo.cjs +0 -2
  160. package/dist/primitives-cNNAeFCo.cjs.map +0 -1
  161. package/dist/primitives-xIr6Tjk4.js +0 -1457
  162. package/dist/primitives-xIr6Tjk4.js.map +0 -1
  163. package/dist/resolver-6B8dI9gr.js +0 -141
  164. package/dist/resolver-6B8dI9gr.js.map +0 -1
  165. package/dist/resolver-DCcmyWSK.cjs +0 -4
  166. package/dist/resolver-DCcmyWSK.cjs.map +0 -1
  167. package/dist/throttle.test.d.ts +0 -2
  168. package/dist/throttle.test.d.ts.map +0 -1
  169. package/dist/transport/generation-window.test.d.ts +0 -2
  170. package/dist/transport/generation-window.test.d.ts.map +0 -1
  171. package/dist/transport/index.cjs +0 -1
  172. package/dist/transport/index.mjs +0 -2
  173. package/dist/transport/packet.test.d.ts +0 -2
  174. package/dist/transport/packet.test.d.ts.map +0 -1
  175. package/dist/transport/quicklz.test.d.ts +0 -2
  176. package/dist/transport/quicklz.test.d.ts.map +0 -1
  177. package/dist/transport/whisper.test.d.ts +0 -2
  178. package/dist/transport/whisper.test.d.ts.map +0 -1
@@ -0,0 +1,208 @@
1
+ import { hash512 } from "../crypto/identity.js";
2
+ import { clampScalar, scalarMultFull, bytesToBigIntLE } from "../crypto/primitives.js";
3
+ import { ed25519 } from "@noble/curves/ed25519.js";
4
+
5
+ // The root key is a 33-byte Ed25519 point
6
+ const LICENSE_ROOT_KEY = new Uint8Array([
7
+ 0xcd, 0x0d, 0xe2, 0xae, 0xd4, 0x63, 0x45, 0x50, 0x9a, 0x7e, 0x3c, 0xfd, 0x8f, 0x68, 0xb3, 0xdc,
8
+ 0x75, 0x55, 0xb2, 0x9d, 0xcc, 0xec, 0x73, 0xcd, 0x18, 0x75, 0x0f, 0x99, 0x38, 0x12, 0x40, 0x8a,
9
+ ]);
10
+
11
+ const enum LicenseBlockType {
12
+ Intermediate = 0,
13
+ Server = 2,
14
+ Ts5Server = 8,
15
+ Ephemeral = 32,
16
+ }
17
+
18
+ interface LicenseBlock {
19
+ readonly key: Uint8Array; // 32-byte Ed25519 point
20
+ readonly hash: Uint8Array; // SHA-512 truncated to 32 bytes
21
+ properties: Uint8Array[];
22
+ issuer: string;
23
+ notValidBefore: Date;
24
+ notValidAfter: Date;
25
+ blockType: LicenseBlockType;
26
+ serverType: number;
27
+ }
28
+
29
+ export class LicenseChain {
30
+ readonly blocks: LicenseBlock[];
31
+
32
+ constructor(blocks: LicenseBlock[]) {
33
+ this.blocks = blocks;
34
+ }
35
+
36
+ /**
37
+ * Derive the session key by chaining Ed25519 point arithmetic starting from
38
+ * the root key through each license block.
39
+ */
40
+ deriveKey(): Uint8Array<ArrayBuffer> {
41
+ let round: Uint8Array<ArrayBuffer> = Uint8Array.from(LICENSE_ROOT_KEY);
42
+ for (const block of this.blocks) {
43
+ round = deriveKeyFromBlock(block, round);
44
+ }
45
+ return round;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * The most blocks a license chain may have. Real chains have two or three —
51
+ * root-issued intermediate, server, ephemeral — and every block costs a
52
+ * point multiplication to derive, so a server's chain of thousands would hold
53
+ * the event loop for seconds.
54
+ */
55
+ export const MAX_LICENSE_BLOCKS = 8;
56
+
57
+ export function parseLicenses(data: Uint8Array): LicenseChain {
58
+ if (data.length < 1) throw new Error("license too short");
59
+ if (data[0] !== 1) throw new Error("unsupported license version");
60
+
61
+ let remaining = data.slice(1);
62
+ const blocks: LicenseBlock[] = [];
63
+
64
+ while (remaining.length > 0) {
65
+ if (blocks.length === MAX_LICENSE_BLOCKS) {
66
+ throw new Error(`license chain longer than ${MAX_LICENSE_BLOCKS} blocks`);
67
+ }
68
+ const { block, consumed } = parseLicenseBlock(remaining);
69
+ blocks.push(block);
70
+ remaining = remaining.slice(consumed);
71
+ }
72
+
73
+ return new LicenseChain(blocks);
74
+ }
75
+
76
+ function parseLicenseBlock(data: Uint8Array): { block: LicenseBlock; consumed: number } {
77
+ const MIN_BLOCK_LEN = 42;
78
+ if (data.length < MIN_BLOCK_LEN) throw new Error("license too short");
79
+ if (data[0] !== 0) throw new Error(`wrong key kind in license: ${data[0]}`);
80
+
81
+ const blockType = data[33] as LicenseBlockType;
82
+
83
+ // Timestamps are seconds since Unix epoch offset by 0x50e22700
84
+ const UNIX_OFFSET = 0x50e22700;
85
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
86
+ const notValidBefore = new Date((view.getUint32(34, false) + UNIX_OFFSET) * 1000);
87
+ const notValidAfter = new Date((view.getUint32(38, false) + UNIX_OFFSET) * 1000);
88
+
89
+ if (notValidAfter < notValidBefore) {
90
+ throw new Error("license times are invalid");
91
+ }
92
+
93
+ const key = Uint8Array.from(data.slice(1, 33));
94
+
95
+ const { payload, payloadRead } = parseBlockPayload(blockType, data, MIN_BLOCK_LEN);
96
+
97
+ const allLen = MIN_BLOCK_LEN + payloadRead;
98
+ if (allLen > data.length) throw new Error("license block longer than the license");
99
+ const hashInput = data.slice(1, allLen);
100
+ const hashFull = hash512(Uint8Array.from(hashInput));
101
+ const hash = Uint8Array.from(hashFull.slice(0, 32));
102
+
103
+ return {
104
+ block: {
105
+ key,
106
+ hash,
107
+ properties: payload.properties,
108
+ issuer: payload.issuer,
109
+ notValidBefore,
110
+ notValidAfter,
111
+ blockType,
112
+ serverType: payload.serverType,
113
+ },
114
+ consumed: allLen,
115
+ };
116
+ }
117
+
118
+ interface BlockPayload {
119
+ issuer: string;
120
+ serverType: number;
121
+ properties: Uint8Array[];
122
+ read: number;
123
+ }
124
+
125
+ function parseBlockPayload(
126
+ blockType: LicenseBlockType,
127
+ data: Uint8Array,
128
+ minBlockLen: number,
129
+ ): { payload: BlockPayload; payloadRead: number } {
130
+ switch (blockType) {
131
+ case LicenseBlockType.Intermediate: {
132
+ const { str, read } = readNullString(data.slice(46));
133
+ return {
134
+ payload: { issuer: str, serverType: 0, properties: [], read: 5 + read },
135
+ payloadRead: 5 + read,
136
+ };
137
+ }
138
+ case LicenseBlockType.Server: {
139
+ const { str, read } = readNullString(data.slice(47));
140
+ return {
141
+ payload: { issuer: str, serverType: data[42] ?? 0, properties: [], read: 6 + read },
142
+ payloadRead: 6 + read,
143
+ };
144
+ }
145
+ case LicenseBlockType.Ts5Server: {
146
+ const propCount = data[43] !== undefined ? data[43] : 0;
147
+ let pos = 44;
148
+ const properties: Uint8Array[] = [];
149
+ for (let i = 0; i < propCount; i++) {
150
+ if (pos >= data.length) throw new Error("license too short");
151
+ const propLen = data[pos++]!;
152
+ if (pos + propLen > data.length) throw new Error("license too short");
153
+ properties.push(Uint8Array.from(data.slice(pos, pos + propLen)));
154
+ pos += propLen;
155
+ }
156
+ return {
157
+ payload: { issuer: "", serverType: data[42] ?? 0, properties, read: pos - minBlockLen },
158
+ payloadRead: pos - minBlockLen,
159
+ };
160
+ }
161
+ case LicenseBlockType.Ephemeral:
162
+ return { payload: { issuer: "", serverType: 0, properties: [], read: 0 }, payloadRead: 0 };
163
+ default:
164
+ throw new Error(`invalid license block type: ${blockType}`);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Scan for a null-terminated string and return its content and the index
170
+ * of the null byte (NOT including it in the count, matching Go behaviour).
171
+ * The parent formulas (`5 + read`, `6 + read`) already add 1 for the null.
172
+ */
173
+ function readNullString(data: Uint8Array): { str: string; read: number } {
174
+ for (let i = 0; i < data.length; i++) {
175
+ if (data[i] === 0) {
176
+ return { str: new TextDecoder().decode(data.slice(0, i)), read: i };
177
+ }
178
+ }
179
+ throw new Error("non-null-terminated issuer string");
180
+ }
181
+
182
+ /**
183
+ * Derive the next key given a parent key and a license block.
184
+ * Mirrors Go's licenseBlock.deriveKey.
185
+ */
186
+ function deriveKeyFromBlock(block: LicenseBlock, parent: Uint8Array): Uint8Array<ArrayBuffer> {
187
+ const scalarBytes = Uint8Array.from(block.hash);
188
+ clampScalar(scalarBytes);
189
+
190
+ // Use raw scalar WITHOUT reducing mod n — NaCl's ge_scalarmult_vartime
191
+ // uses the raw 256-bit scalar. Reducing mod n changes the result when
192
+ // the point has a small-order component (Ed25519 cofactor = 8).
193
+ const scalar = bytesToBigIntLE(scalarBytes);
194
+
195
+ const pub = ed25519.Point.fromBytes(block.key);
196
+ const negPub = pub.negate();
197
+
198
+ const par = ed25519.Point.fromBytes(parent);
199
+ const negPar = par.negate();
200
+
201
+ const res = scalarMultFull(negPub, scalar).add(negPar);
202
+ const raw = res.toBytes();
203
+ const final = new Uint8Array(raw.length) as Uint8Array<ArrayBuffer>;
204
+ final.set(raw);
205
+ final[31] = (final[31] !== undefined ? final[31] : 0) ^ 0x80;
206
+
207
+ return final;
208
+ }
@@ -0,0 +1,136 @@
1
+ import { createHash } from "node:crypto";
2
+ import { buildCommandOrdered, buildCommand } from "@honeybbq/teamspeak-core/internal";
3
+ import { sign, generateTemporaryKey } from "./crypto/primitives.js";
4
+ import { identityPrivateKey } from "./crypto/identity.js";
5
+ import { parseServerCapabilities } from "./capabilities.js";
6
+ import { cryptoInit2 } from "./handshake/crypt-init2.js";
7
+ import { PacketType } from "./transport/packet.js";
8
+ import type { ClientInternals } from "./client.js";
9
+
10
+ /** Handle the `clientinitiv` message (P-256 based crypto path). */
11
+ export function handleHandshakeInitIV(
12
+ client: ClientInternals,
13
+ params: Record<string, string>,
14
+ ): void {
15
+ const alpha = params["alpha"] ?? "";
16
+ const beta = params["beta"] ?? "";
17
+ const omega = params["omega"] ?? "";
18
+
19
+ client.crypt.initCrypto(alpha, beta, omega);
20
+ client.logger.info("crypto initialized (P-256 path), sending clientinit");
21
+ sendClientInit(client);
22
+ }
23
+
24
+ /** Handle the `initivexpand2` message (Ed25519 / TS3 crypto path). */
25
+ export function handleHandshakeExpand2(
26
+ client: ClientInternals,
27
+ params: Record<string, string>,
28
+ ): void {
29
+ client.logger.info("received initivexpand2");
30
+ client.handler.receivedFinalInitAck();
31
+
32
+ const license = params["l"] ?? "";
33
+ const omega = params["omega"] ?? "";
34
+ const proof = params["proof"] ?? "";
35
+ const beta = params["beta"] ?? "";
36
+
37
+ const privateKey = sendClientEkPacket(client, beta);
38
+ cryptoInit2(client.crypt, license, omega, proof, beta, privateKey);
39
+ sendClientInit(client);
40
+ }
41
+
42
+ /** Handle `initserver` — marks the client as connected. */
43
+ export function handleInitServer(client: ClientInternals, params: Record<string, string>): void {
44
+ const idStr = params["aclid"] ?? params["clid"] ?? "";
45
+ const clid = idStr ? parseInt(idStr, 10) : 0;
46
+
47
+ if (clid > 0) {
48
+ client.clid = clid;
49
+ client.handler.setClientID(clid);
50
+ }
51
+
52
+ // initserver is the only place the server names itself, and it determines how
53
+ // the server hashes our public key into client_unique_identifier: SHA-1 on
54
+ // TS3, SHA-256 on TS6. Recording the whole capability set here keeps callers
55
+ // from re-deriving it, or assuming one server generation.
56
+ client.capabilities = parseServerCapabilities(params);
57
+
58
+ client.logger.info("connected to server", { selfId: client.clid });
59
+ client.markConnected();
60
+
61
+ // Inform the server about mute state
62
+ setImmediate(() => {
63
+ const updateCmd = buildCommand("clientupdate", {
64
+ client_input_muted: "0",
65
+ client_output_muted: "0",
66
+ });
67
+ client.execNoWait(updateCmd).catch(() => {});
68
+ });
69
+ }
70
+
71
+ function sendClientEkPacket(client: ClientInternals, beta: string): Uint8Array {
72
+ const [publicKey, privateKey] = generateTemporaryKey();
73
+ const ekBase64 = Buffer.from(publicKey).toString("base64");
74
+ const clientProof = buildClientEkProof(client, publicKey, beta);
75
+
76
+ const clientEk = buildCommandOrdered("clientek", [
77
+ ["ek", ekBase64],
78
+ ["proof", clientProof],
79
+ ]);
80
+ client.handler.sendPacket(PacketType.Command, Buffer.from(clientEk), 0);
81
+ return privateKey;
82
+ }
83
+
84
+ function buildClientEkProof(client: ClientInternals, publicKey: Uint8Array, beta: string): string {
85
+ const betaBytes = Buffer.from(beta, "base64");
86
+ const toSign = new Uint8Array(86);
87
+ toSign.set(publicKey.slice(0, 32));
88
+ toSign.set(betaBytes.slice(0, Math.min(54, betaBytes.length)), 32);
89
+ const sig = sign(identityPrivateKey(client.crypt.identity), toSign);
90
+ return Buffer.from(sig).toString("base64");
91
+ }
92
+
93
+ /**
94
+ * Encode a password field for the command protocol.
95
+ *
96
+ * TeamSpeak expects password fields as base64(sha1(password)) — server and
97
+ * channel passwords during the handshake, the `clientmove` channel password,
98
+ * and the `cpw` field of the file transfer commands. Sending the plain text
99
+ * instead makes the server reject correct passwords with error 781.
100
+ */
101
+ export function prepareClientPassword(password: string): string {
102
+ if (password === "") return "";
103
+ return createHash("sha1").update(password).digest().toString("base64");
104
+ }
105
+
106
+ export function sendClientInit(client: ClientInternals): void {
107
+ const cmd = buildClientInitCommand(client);
108
+ client.handler.sendPacket(PacketType.Command, Buffer.from(cmd), 0);
109
+ }
110
+
111
+ function buildClientInitCommand(client: ClientInternals): string {
112
+ const pubKeyBase64 = client.crypt.identity.publicKey();
113
+ const initOptions = client.initOptions;
114
+ // HWID should match TS3 client UID format: base64(SHA1(publicKey()))
115
+ const hwid = createHash("sha1").update(pubKeyBase64).digest().toString("base64");
116
+
117
+ return buildCommandOrdered("clientinit", [
118
+ ["client_nickname", client.nickname],
119
+ ["client_version", "3.?.? [Build: 5680278000]"],
120
+ ["client_platform", "Windows"],
121
+ ["client_input_hardware", "1"],
122
+ ["client_output_hardware", "1"],
123
+ ["client_default_channel", initOptions.defaultChannel],
124
+ ["client_default_channel_password", initOptions.defaultChannelPasswordHash],
125
+ ["client_server_password", initOptions.serverPasswordHash],
126
+ ["client_meta_data", ""],
127
+ [
128
+ "client_version_sign",
129
+ "DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg==",
130
+ ],
131
+ ["client_key_offset", String(client.crypt.identity.offset)],
132
+ ["client_nickname_phonetic", ""],
133
+ ["client_default_token", ""],
134
+ ["hwid", hwid],
135
+ ]);
136
+ }
package/src/helpers.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Reports whether `actual` equals `expected` or equals `expected` followed by
3
+ * only digits — the pattern TeamSpeak uses when a nickname is already taken.
4
+ */
5
+ export function isAutoNicknameMatch(expected: string, actual: string): boolean {
6
+ if (actual === expected) return true;
7
+ if (!actual.startsWith(expected)) return false;
8
+ const suffix = actual.slice(expected.length);
9
+ return /^\d+$/.test(suffix);
10
+ }
package/src/index.ts ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * A clean-room TeamSpeak client: it connects over the voice protocol, the way
3
+ * the TeamSpeak application does.
4
+ *
5
+ * Everything in `@honeybbq/teamspeak-core` — the typed commands, ids, errors —
6
+ * is re-exported here, so this is the only package a voice client needs.
7
+ */
8
+
9
+ export * from "@honeybbq/teamspeak-core";
10
+
11
+ // The client
12
+ export { Client, ClientStatus } from "./client.js";
13
+
14
+ export type {
15
+ TextMessageEvent,
16
+ ClientMovedEvent,
17
+ ClientLeaveEvent,
18
+ KickedEvent,
19
+ NotificationEvent,
20
+ ClientEnterEvent,
21
+ PokeEvent,
22
+ VoiceDataEvent,
23
+ EventMap,
24
+ EventMiddleware,
25
+ Listener,
26
+ ServerEvent,
27
+ ServerEventName,
28
+ Unsubscribe,
29
+ AddrResolver,
30
+ ClientOptions,
31
+ ResolvedAddr,
32
+ DatagramSocket,
33
+ SocketFactory,
34
+ FileTransferSocketFactory,
35
+ } from "./types.js";
36
+
37
+ export {
38
+ AlreadyConnectedError,
39
+ FileTransferError,
40
+ CryptoInitError,
41
+ InvalidAddressError,
42
+ InvalidIdentityError,
43
+ RemovedError,
44
+ ConnectionTimeoutError,
45
+ } from "./errors.js";
46
+
47
+ // Commands only the voice protocol has.
48
+ export * from "./api/voice.js";
49
+
50
+ // File transfer data
51
+ export { dialFileTransfer, uploadFileData, downloadFileData } from "./transfer.js";
52
+ export type { FileTransferOptions } from "./transfer.js";
53
+
54
+ // Server capabilities, read from the handshake
55
+ export { unknownServerCapabilities } from "./capabilities.js";
56
+ export type { ServerCapabilities, ServerFamily } from "./capabilities.js";
57
+
58
+ // Tracing points for diagnostics_channel subscribers
59
+ export { commandChannel, packetChannel } from "./diagnostics.js";
60
+ export type { CommandTrace, PacketTrace } from "./diagnostics.js";
61
+
62
+ // Reconnection policy
63
+ export { backoffDelay, defaultShouldRetry, defaultBackoffPolicy } from "./reconnect.js";
64
+ export type { ReconnectOptions, BackoffPolicy } from "./reconnect.js";
65
+
66
+ // Identity
67
+ export {
68
+ Identity,
69
+ parseIdentity,
70
+ generateIdentity,
71
+ generateIdentitySync,
72
+ uidFromPublicKey,
73
+ } from "./crypto/index.js";
74
+ export type { UIDAlgorithm } from "./crypto/index.js";
@@ -0,0 +1,136 @@
1
+ import type { Command } from "@honeybbq/teamspeak-core/internal";
2
+ import type {
3
+ ClientEnterEvent,
4
+ ClientLeaveEvent,
5
+ ClientMovedEvent,
6
+ TextMessageEvent,
7
+ PokeEvent,
8
+ } from "./types.js";
9
+ import {
10
+ parseClient,
11
+ parseUint64,
12
+ parseUint16,
13
+ parseInt10,
14
+ } from "@honeybbq/teamspeak-core/internal";
15
+ import { asChannelID, asClientID, asUID } from "@honeybbq/teamspeak-core";
16
+
17
+ export type NotificationResult =
18
+ | { kind: "clientEnter"; info: ClientEnterEvent }
19
+ | { kind: "clientLeave"; event: ClientLeaveEvent; isSelf: boolean }
20
+ | { kind: "clientMoved"; event: ClientMovedEvent; reasonMessage: string }
21
+ | { kind: "textMessage"; message: TextMessageEvent }
22
+ | { kind: "poked"; event: PokeEvent }
23
+ | { kind: "unknown" };
24
+
25
+ export function handleNotification(
26
+ cmd: Command,
27
+ selfCLID: number,
28
+ clients: Map<number, ClientEnterEvent>,
29
+ _nickname: string,
30
+ ): NotificationResult {
31
+ switch (cmd.name) {
32
+ case "notifycliententerview":
33
+ return handleClientEnterView(cmd, clients);
34
+ case "notifyclientleftview":
35
+ return handleClientLeftView(cmd, selfCLID, clients);
36
+ case "notifyclientmoved":
37
+ return handleClientMoved(cmd, clients);
38
+ case "notifytextmessage":
39
+ return handleTextMessage(cmd, clients);
40
+ case "notifyclientpoke":
41
+ return handleClientPoked(cmd);
42
+ default:
43
+ return { kind: "unknown" };
44
+ }
45
+ }
46
+
47
+ function handleClientEnterView(
48
+ cmd: Command,
49
+ clients: Map<number, ClientEnterEvent>,
50
+ ): NotificationResult {
51
+ const info = parseClient(cmd.params);
52
+ // No client has id 0: a row without one describes nobody.
53
+ if (info.id === 0) return { kind: "unknown" };
54
+ clients.set(info.id, info);
55
+ return { kind: "clientEnter", info };
56
+ }
57
+
58
+ function handleClientLeftView(
59
+ cmd: Command,
60
+ selfCLID: number,
61
+ clients: Map<number, ClientEnterEvent>,
62
+ ): NotificationResult {
63
+ const clid = asClientID(parseUint16(cmd.params["clid"] ?? ""));
64
+ if (clid === 0) return { kind: "unknown" };
65
+ const isSelf = clid === selfCLID;
66
+ clients.delete(clid);
67
+ return {
68
+ kind: "clientLeave",
69
+ event: {
70
+ clientID: clid,
71
+ reason: parseInt10(cmd.params["reasonid"] ?? ""),
72
+ reasonMessage: cmd.params["reasonmsg"] ?? "",
73
+ invokerID: asClientID(parseUint16(cmd.params["invokerid"] ?? "")),
74
+ invokerName: cmd.params["invokername"] ?? "",
75
+ invokerUID: asUID(cmd.params["invokeruid"] ?? ""),
76
+ },
77
+ isSelf,
78
+ };
79
+ }
80
+
81
+ function handleClientMoved(
82
+ cmd: Command,
83
+ clients: Map<number, ClientEnterEvent>,
84
+ ): NotificationResult {
85
+ const clid = asClientID(parseUint16(cmd.params["clid"] ?? ""));
86
+ const ctid = asChannelID(parseUint64(cmd.params["ctid"] ?? ""));
87
+ if (clid === 0) return { kind: "unknown" };
88
+
89
+ const existing = clients.get(clid);
90
+ if (existing) clients.set(clid, { ...existing, channelID: ctid });
91
+
92
+ return {
93
+ kind: "clientMoved",
94
+ event: {
95
+ clientID: clid,
96
+ targetChannelID: ctid,
97
+ reason: parseInt10(cmd.params["reasonid"] ?? ""),
98
+ invokerID: asClientID(parseUint16(cmd.params["invokerid"] ?? "")),
99
+ invokerName: cmd.params["invokername"] ?? "",
100
+ invokerUID: asUID(cmd.params["invokeruid"] ?? ""),
101
+ },
102
+ reasonMessage: cmd.params["reasonmsg"] ?? "",
103
+ };
104
+ }
105
+
106
+ function handleTextMessage(
107
+ cmd: Command,
108
+ clients: Map<number, ClientEnterEvent>,
109
+ ): NotificationResult {
110
+ const invokerID = asClientID(parseUint16(cmd.params["invokerid"] ?? ""));
111
+ const invokerInfo = clients.get(invokerID);
112
+
113
+ const message: TextMessageEvent = {
114
+ targetMode: parseInt10(cmd.params["targetmode"] ?? ""),
115
+ targetID: asClientID(parseUint16(cmd.params["target"] ?? "")),
116
+ invokerID,
117
+ invokerName: cmd.params["invokername"] ?? "",
118
+ invokerUID: asUID(cmd.params["invokeruid"] ?? invokerInfo?.uid ?? ""),
119
+ message: cmd.params["msg"] ?? "",
120
+ invokerGroups: invokerInfo?.serverGroups ?? [],
121
+ };
122
+
123
+ return { kind: "textMessage", message };
124
+ }
125
+
126
+ function handleClientPoked(cmd: Command): NotificationResult {
127
+ return {
128
+ kind: "poked",
129
+ event: {
130
+ invokerID: asClientID(parseUint16(cmd.params["invokerid"] ?? "")),
131
+ invokerName: cmd.params["invokername"] ?? "",
132
+ invokerUID: asUID(cmd.params["invokeruid"] ?? ""),
133
+ message: cmd.params["msg"] ?? "",
134
+ },
135
+ };
136
+ }