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

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 (151) hide show
  1. package/README.md +345 -64
  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 +173 -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 +3996 -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/generation-window.d.ts +15 -0
  50. package/dist/transport/generation-window.d.ts.map +1 -1
  51. package/dist/transport/handler.d.ts +44 -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 -56
  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 +1393 -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.fuzz.test.ts +87 -0
  79. package/src/handshake/license.test.ts +49 -0
  80. package/src/handshake/license.ts +208 -0
  81. package/src/handshake.ts +137 -0
  82. package/src/helpers.ts +10 -0
  83. package/src/index.ts +74 -0
  84. package/src/notifications.ts +136 -0
  85. package/src/reconnect.ts +141 -0
  86. package/src/throttle.ts +92 -0
  87. package/src/transfer.ts +127 -0
  88. package/src/transport/generation-window.ts +108 -0
  89. package/src/transport/handler.ts +917 -0
  90. package/src/transport/index.ts +14 -0
  91. package/src/transport/packet.ts +69 -0
  92. package/src/transport/quicklz.ts +209 -0
  93. package/src/types.ts +350 -0
  94. package/dist/address-BR-5unVo.cjs +0 -2
  95. package/dist/address-BR-5unVo.cjs.map +0 -1
  96. package/dist/address-BfmovVpp.js +0 -65
  97. package/dist/address-BfmovVpp.js.map +0 -1
  98. package/dist/api.d.ts +0 -17
  99. package/dist/api.d.ts.map +0 -1
  100. package/dist/command/command.d.ts +0 -12
  101. package/dist/command/command.d.ts.map +0 -1
  102. package/dist/command/index.cjs +0 -1
  103. package/dist/command/index.d.ts +0 -4
  104. package/dist/command/index.d.ts.map +0 -1
  105. package/dist/command/index.mjs +0 -3
  106. package/dist/command/parser.d.ts +0 -3
  107. package/dist/command/parser.d.ts.map +0 -1
  108. package/dist/command-BLe-C2L2.js +0 -74
  109. package/dist/command-BLe-C2L2.js.map +0 -1
  110. package/dist/command-DAK4pUBP.cjs +0 -4
  111. package/dist/command-DAK4pUBP.cjs.map +0 -1
  112. package/dist/crypt-handshake-CQkQreLx.cjs +0 -2
  113. package/dist/crypt-handshake-CQkQreLx.cjs.map +0 -1
  114. package/dist/crypt-handshake-DziOY4Lq.js +0 -55
  115. package/dist/crypt-handshake-DziOY4Lq.js.map +0 -1
  116. package/dist/crypt-init2-D70ha4LB.js +0 -158
  117. package/dist/crypt-init2-D70ha4LB.js.map +0 -1
  118. package/dist/crypt-init2-DparLekm.cjs +0 -2
  119. package/dist/crypt-init2-DparLekm.cjs.map +0 -1
  120. package/dist/crypto/index.cjs +0 -1
  121. package/dist/crypto/index.mjs +0 -3
  122. package/dist/crypto-Bg02m4ZE.js +0 -165
  123. package/dist/crypto-Bg02m4ZE.js.map +0 -1
  124. package/dist/crypto-CDlweUL3.cjs +0 -2
  125. package/dist/crypto-CDlweUL3.cjs.map +0 -1
  126. package/dist/discovery/index.cjs +0 -1
  127. package/dist/discovery/index.mjs +0 -2
  128. package/dist/handler-DI_abe9c.js +0 -483
  129. package/dist/handler-DI_abe9c.js.map +0 -1
  130. package/dist/handler-DuwoO5RC.cjs +0 -2
  131. package/dist/handler-DuwoO5RC.cjs.map +0 -1
  132. package/dist/handshake/index.cjs +0 -1
  133. package/dist/handshake/index.mjs +0 -3
  134. package/dist/index.cjs +0 -4
  135. package/dist/index.cjs.map +0 -1
  136. package/dist/index.mjs +0 -827
  137. package/dist/index.mjs.map +0 -1
  138. package/dist/parser-CL33R9tr.cjs +0 -2
  139. package/dist/parser-CL33R9tr.cjs.map +0 -1
  140. package/dist/parser-DZMv3Jm-.js +0 -33
  141. package/dist/parser-DZMv3Jm-.js.map +0 -1
  142. package/dist/primitives-cNNAeFCo.cjs +0 -2
  143. package/dist/primitives-cNNAeFCo.cjs.map +0 -1
  144. package/dist/primitives-xIr6Tjk4.js +0 -1457
  145. package/dist/primitives-xIr6Tjk4.js.map +0 -1
  146. package/dist/resolver-6B8dI9gr.js +0 -141
  147. package/dist/resolver-6B8dI9gr.js.map +0 -1
  148. package/dist/resolver-DCcmyWSK.cjs +0 -4
  149. package/dist/resolver-DCcmyWSK.cjs.map +0 -1
  150. package/dist/transport/index.cjs +0 -1
  151. package/dist/transport/index.mjs +0 -2
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import fc from "fast-check";
3
+ import { ed25519 } from "@noble/curves/ed25519.js";
4
+ import { MAX_LICENSE_BLOCKS, parseLicenses } from "./license.js";
5
+
6
+ // The license chain is the server's, sent before the handshake has proved
7
+ // anything: any bytes can arrive in it. Reading it must end in a chain of a
8
+ // bounded number of blocks or in an Error, and deriving a key from a chain
9
+ // that read must give a key or an Error, never anything else.
10
+
11
+ const BASE_POINT = ed25519.Point.BASE.toBytes();
12
+
13
+ const key = fc.oneof(fc.constant(BASE_POINT), fc.uint8Array({ minLength: 32, maxLength: 32 }));
14
+ const times = fc
15
+ .tuple(fc.nat(0xffff_ffff), fc.nat(0xffff_ffff))
16
+ .map(([a, b]): [number, number] => [Math.min(a, b), Math.max(a, b)]);
17
+ const issuer = fc.uint8Array({ maxLength: 12 }).map((bytes) => bytes.map((b) => b || 1));
18
+
19
+ /** One block, laid out as the parser reads its type, with a length that fits it. */
20
+ const block = fc
21
+ .tuple(
22
+ key,
23
+ times,
24
+ fc.constantFrom(0, 2, 8, 32, 7),
25
+ fc.uint8Array({ minLength: 6, maxLength: 6 }),
26
+ issuer,
27
+ fc.array(fc.uint8Array({ maxLength: 6 }), { maxLength: 4 }),
28
+ )
29
+ .map(([point, [before, after], type, fixed, name, properties]) => {
30
+ const head = new Uint8Array(42);
31
+ head.set(point, 1);
32
+ head[33] = type;
33
+ new DataView(head.buffer).setUint32(34, before, false);
34
+ new DataView(head.buffer).setUint32(38, after, false);
35
+ const text = Uint8Array.from([...name, 0]);
36
+ switch (type) {
37
+ case 0:
38
+ return Uint8Array.from([...head, ...fixed.subarray(0, 4), ...text]);
39
+ case 2:
40
+ return Uint8Array.from([...head, ...fixed.subarray(0, 5), ...text]);
41
+ case 8:
42
+ return Uint8Array.from([
43
+ ...head,
44
+ fixed[0]!,
45
+ properties.length,
46
+ ...properties.flatMap((p) => [p.length, ...p]),
47
+ ]);
48
+ default:
49
+ return head;
50
+ }
51
+ });
52
+
53
+ const chain = fc
54
+ .tuple(fc.constantFrom(1, 1, 1, 0, 2), fc.array(block, { maxLength: MAX_LICENSE_BLOCKS + 1 }))
55
+ .map(([version, blocks]) => Uint8Array.from([version, ...blocks.flatMap((b) => [...b])]));
56
+
57
+ /** A well-formed chain with some bytes changed or cut off. */
58
+ const damaged = fc
59
+ .tuple(chain, fc.array(fc.tuple(fc.nat(400), fc.nat(255)), { maxLength: 4 }), fc.nat(400))
60
+ .map(([data, edits, cut]) => {
61
+ const out = Uint8Array.from(data);
62
+ for (const [at, byte] of edits) if (at < out.length) out[at] = byte;
63
+ return cut < out.length && cut % 3 === 0 ? out.subarray(0, cut) : out;
64
+ });
65
+
66
+ describe("a license chain from the server", () => {
67
+ it("reads to a bounded chain or an Error, and derives a key or an Error", () => {
68
+ fc.assert(
69
+ fc.property(fc.oneof(chain, damaged, fc.uint8Array({ maxLength: 200 })), (data) => {
70
+ let parsed;
71
+ try {
72
+ parsed = parseLicenses(data);
73
+ } catch (error) {
74
+ expect(error).toBeInstanceOf(Error);
75
+ return;
76
+ }
77
+ expect(parsed.blocks.length).toBeLessThanOrEqual(MAX_LICENSE_BLOCKS);
78
+ try {
79
+ expect(parsed.deriveKey()).toHaveLength(32);
80
+ } catch (error) {
81
+ expect(error).toBeInstanceOf(Error);
82
+ }
83
+ }),
84
+ { numRuns: 300 },
85
+ );
86
+ });
87
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { ed25519 } from "@noble/curves/ed25519.js";
3
+ import { MAX_LICENSE_BLOCKS, parseLicenses } from "./license.js";
4
+
5
+ const BASE_POINT = ed25519.Point.BASE.toBytes();
6
+ const EPHEMERAL_BLOCK_LEN = 42;
7
+
8
+ /** A version byte and `count` ephemeral blocks, the smallest a block can be. */
9
+ function ephemeralChain(count: number): Uint8Array {
10
+ const data = new Uint8Array(1 + count * EPHEMERAL_BLOCK_LEN);
11
+ data[0] = 1;
12
+ for (let i = 0; i < count; i++) {
13
+ const offset = 1 + i * EPHEMERAL_BLOCK_LEN;
14
+ data[offset] = 0; // key kind
15
+ data.set(BASE_POINT, offset + 1);
16
+ data[offset + 33] = 32; // ephemeral
17
+ }
18
+ return data;
19
+ }
20
+
21
+ describe("parseLicenses", () => {
22
+ it(`takes a chain of up to ${MAX_LICENSE_BLOCKS} blocks`, () => {
23
+ const chain = parseLicenses(ephemeralChain(MAX_LICENSE_BLOCKS));
24
+ expect(chain.blocks).toHaveLength(MAX_LICENSE_BLOCKS);
25
+ expect(chain.deriveKey()).toHaveLength(32);
26
+ });
27
+
28
+ // Every block costs a point multiplication to derive: a thousand of them
29
+ // held the event loop for seconds.
30
+ it("rejects a longer chain before deriving anything", () => {
31
+ expect(() => parseLicenses(ephemeralChain(MAX_LICENSE_BLOCKS + 1))).toThrow(/longer than/);
32
+ expect(() => parseLicenses(ephemeralChain(1000))).toThrow(/longer than/);
33
+ });
34
+
35
+ it("rejects a block that declares more than the license holds", () => {
36
+ // A TS5 server block whose property count, at byte 43, is cut off.
37
+ const ts5 = ephemeralChain(1);
38
+ ts5[34] = 8;
39
+ expect(() => parseLicenses(ts5)).toThrow(/longer than the license/);
40
+
41
+ // One whose property claims 200 bytes when 1 follows.
42
+ const property = new Uint8Array(1 + 42 + 4);
43
+ property.set(ephemeralChain(1));
44
+ property[34] = 8;
45
+ property[44] = 1; // one property
46
+ property[45] = 200;
47
+ expect(() => parseLicenses(property)).toThrow(/too short/);
48
+ });
49
+ });
@@ -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,137 @@
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.handler.receivedFinalInitAck();
20
+ client.crypt.initCrypto(alpha, beta, omega);
21
+ client.logger.info("crypto initialized (P-256 path), sending clientinit");
22
+ sendClientInit(client);
23
+ }
24
+
25
+ /** Handle the `initivexpand2` message (Ed25519 / TS3 crypto path). */
26
+ export function handleHandshakeExpand2(
27
+ client: ClientInternals,
28
+ params: Record<string, string>,
29
+ ): void {
30
+ client.logger.info("received initivexpand2");
31
+ client.handler.receivedFinalInitAck();
32
+
33
+ const license = params["l"] ?? "";
34
+ const omega = params["omega"] ?? "";
35
+ const proof = params["proof"] ?? "";
36
+ const beta = params["beta"] ?? "";
37
+
38
+ const privateKey = sendClientEkPacket(client, beta);
39
+ cryptoInit2(client.crypt, license, omega, proof, beta, privateKey);
40
+ sendClientInit(client);
41
+ }
42
+
43
+ /** Handle `initserver` — marks the client as connected. */
44
+ export function handleInitServer(client: ClientInternals, params: Record<string, string>): void {
45
+ const idStr = params["aclid"] ?? params["clid"] ?? "";
46
+ const clid = idStr ? parseInt(idStr, 10) : 0;
47
+
48
+ if (clid > 0) {
49
+ client.clid = clid;
50
+ client.handler.setClientID(clid);
51
+ }
52
+
53
+ // initserver is the only place the server names itself, and it determines how
54
+ // the server hashes our public key into client_unique_identifier: SHA-1 on
55
+ // TS3, SHA-256 on TS6. Recording the whole capability set here keeps callers
56
+ // from re-deriving it, or assuming one server generation.
57
+ client.capabilities = parseServerCapabilities(params);
58
+
59
+ client.logger.info("connected to server", { selfId: client.clid });
60
+ client.markConnected();
61
+
62
+ // Inform the server about mute state
63
+ setImmediate(() => {
64
+ const updateCmd = buildCommand("clientupdate", {
65
+ client_input_muted: "0",
66
+ client_output_muted: "0",
67
+ });
68
+ client.execNoWait(updateCmd).catch(() => {});
69
+ });
70
+ }
71
+
72
+ function sendClientEkPacket(client: ClientInternals, beta: string): Uint8Array {
73
+ const [publicKey, privateKey] = generateTemporaryKey();
74
+ const ekBase64 = Buffer.from(publicKey).toString("base64");
75
+ const clientProof = buildClientEkProof(client, publicKey, beta);
76
+
77
+ const clientEk = buildCommandOrdered("clientek", [
78
+ ["ek", ekBase64],
79
+ ["proof", clientProof],
80
+ ]);
81
+ client.handler.sendPacket(PacketType.Command, Buffer.from(clientEk), 0);
82
+ return privateKey;
83
+ }
84
+
85
+ function buildClientEkProof(client: ClientInternals, publicKey: Uint8Array, beta: string): string {
86
+ const betaBytes = Buffer.from(beta, "base64");
87
+ const toSign = new Uint8Array(86);
88
+ toSign.set(publicKey.slice(0, 32));
89
+ toSign.set(betaBytes.slice(0, Math.min(54, betaBytes.length)), 32);
90
+ const sig = sign(identityPrivateKey(client.crypt.identity), toSign);
91
+ return Buffer.from(sig).toString("base64");
92
+ }
93
+
94
+ /**
95
+ * Encode a password field for the command protocol.
96
+ *
97
+ * TeamSpeak expects password fields as base64(sha1(password)) — server and
98
+ * channel passwords during the handshake, the `clientmove` channel password,
99
+ * and the `cpw` field of the file transfer commands. Sending the plain text
100
+ * instead makes the server reject correct passwords with error 781.
101
+ */
102
+ export function prepareClientPassword(password: string): string {
103
+ if (password === "") return "";
104
+ return createHash("sha1").update(password).digest().toString("base64");
105
+ }
106
+
107
+ export function sendClientInit(client: ClientInternals): void {
108
+ const cmd = buildClientInitCommand(client);
109
+ client.handler.sendPacket(PacketType.Command, Buffer.from(cmd), 0);
110
+ }
111
+
112
+ function buildClientInitCommand(client: ClientInternals): string {
113
+ const pubKeyBase64 = client.crypt.identity.publicKey();
114
+ const initOptions = client.initOptions;
115
+ // HWID should match TS3 client UID format: base64(SHA1(publicKey()))
116
+ const hwid = createHash("sha1").update(pubKeyBase64).digest().toString("base64");
117
+
118
+ return buildCommandOrdered("clientinit", [
119
+ ["client_nickname", client.nickname],
120
+ ["client_version", "3.?.? [Build: 5680278000]"],
121
+ ["client_platform", "Windows"],
122
+ ["client_input_hardware", "1"],
123
+ ["client_output_hardware", "1"],
124
+ ["client_default_channel", initOptions.defaultChannel],
125
+ ["client_default_channel_password", initOptions.defaultChannelPasswordHash],
126
+ ["client_server_password", initOptions.serverPasswordHash],
127
+ ["client_meta_data", ""],
128
+ [
129
+ "client_version_sign",
130
+ "DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg==",
131
+ ],
132
+ ["client_key_offset", String(client.crypt.identity.offset)],
133
+ ["client_nickname_phonetic", ""],
134
+ ["client_default_token", ""],
135
+ ["hwid", hwid],
136
+ ]);
137
+ }
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";