@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
package/README.md CHANGED
@@ -4,15 +4,19 @@
4
4
 
5
5
  **A clean-room TeamSpeak client protocol library written in pure TypeScript.**
6
6
 
7
- Compatible with TeamSpeak 3, 5 & 6. No proprietary SDK. No copy-pasted code.
7
+ Works with TeamSpeak 3 and TeamSpeak 6 servers. No proprietary SDK, no copied code.
8
8
 
9
9
  [![CI](https://github.com/honeybbq/teamspeak-js/actions/workflows/ci.yml/badge.svg)](https://github.com/honeybbq/teamspeak-js/actions/workflows/ci.yml)
10
10
  [![npm](https://img.shields.io/npm/v/@honeybbq/teamspeak-client)](https://www.npmjs.com/package/@honeybbq/teamspeak-client)
11
+ [![npm beta](https://img.shields.io/npm/v/@honeybbq/teamspeak-client/beta?label=beta)](https://www.npmjs.com/package/@honeybbq/teamspeak-client?activeTab=versions)
12
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/HoneyBBQ/teamspeak-js/badge)](https://scorecard.dev/viewer/?uri=github.com/HoneyBBQ/teamspeak-js)
11
13
  [![Node Version](https://img.shields.io/node/v/@honeybbq/teamspeak-client)](package.json)
12
14
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
13
15
 
14
16
  </div>
15
17
 
18
+ Upgrading from 0.x? See the [migration guide](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/migrating-to-1.0.md).
19
+
16
20
  ## Features
17
21
 
18
22
  - **Full protocol handshake** — ECDH key exchange, RSA puzzle, EAX-encrypted transport
@@ -20,22 +24,29 @@ Compatible with TeamSpeak 3, 5 & 6. No proprietary SDK. No copy-pasted code.
20
24
  - **Event-driven API** — Register handlers for text messages, client enter/leave, channel moves, kicks, etc.
21
25
  - **Voice data** — Send Opus voice packets (codec 4 & 5)
22
26
  - **File transfers** — Upload, download, and delete files on the server
23
- - **Address resolution** — SRV records, TSDNS, and direct address support
27
+ - **Address resolution** — SRV records, TSDNS, and direct addresses; server nicknames on request (`resolveNicknames: true`), since that lookup goes to named.myteamspeak.com
24
28
  - **Middleware** — Pluggable command and event middleware chains
25
29
  - **Built-in rate limiter** — Token-bucket throttling to prevent server-side flood kicks
26
30
  - **Identity management** — Generate, import/export, and upgrade security level of identities
27
- - **Dual format** — Ships ESM and CJS with full TypeScript declarations
31
+ - **Full command set** — 111 typed functions covering every command the voice protocol answers
32
+ - **Reconnection** — Opt-in retry with backoff, tuned for TeamSpeak's antiflood ban
33
+ - **Server capabilities** — Reports the server version and the uid hash it uses (SHA-1 on TS3, SHA-256 on TS6), and the server's name, icon, banner and welcome message
34
+ - **Custom transports** — Supply your own socket, for proxies, tunnels or tests
35
+ - **ESM with types** — Ships ES modules with full TypeScript declarations
28
36
  - **Zero native deps** — Pure TypeScript, no native addons required
29
37
 
30
38
  ## Installation
31
39
 
40
+ 1.0 is in beta, published under the `beta` tag. A plain `npm install` still
41
+ gets 0.3, so ask for the beta:
42
+
32
43
  ```bash
33
- npm install @honeybbq/teamspeak-client
44
+ npm install @honeybbq/teamspeak-client@beta
34
45
  # or
35
- pnpm add @honeybbq/teamspeak-client
46
+ pnpm add @honeybbq/teamspeak-client@beta
36
47
  ```
37
48
 
38
- Requires **Node.js 20.19** or later.
49
+ Everything in this README is the 1.0 API. You need Node.js 20.19 or newer.
39
50
 
40
51
  ## Quick Start
41
52
 
@@ -43,7 +54,7 @@ Requires **Node.js 20.19** or later.
43
54
  import { Client, generateIdentity } from "@honeybbq/teamspeak-client";
44
55
 
45
56
  // Generate a new identity (or load an existing one)
46
- const identity = generateIdentity(8);
57
+ const identity = await generateIdentity(8);
47
58
 
48
59
  // Create the client
49
60
  const client = new Client(identity, "localhost", "TSBot", {
@@ -65,11 +76,8 @@ client.on("disconnected", (err) => {
65
76
  console.log("Disconnected:", err?.message ?? "clean");
66
77
  });
67
78
 
68
- // Connect
69
- await client.connect();
70
-
71
- // Wait until connected (with 15s timeout)
72
- await client.waitConnected(AbortSignal.timeout(15_000));
79
+ // Connect; resolves once the handshake completes.
80
+ await client.connect({ signal: AbortSignal.timeout(15_000) });
73
81
 
74
82
  // Stay connected until interrupted
75
83
  process.on("SIGINT", async () => {
@@ -81,12 +89,26 @@ process.on("SIGINT", async () => {
81
89
 
82
90
  ### Client Lifecycle
83
91
 
84
- | Method | Description |
85
- | --------------------------------------------- | ----------------------------------- |
86
- | `new Client(identity, addr, nickname, opts?)` | Create a new client |
87
- | `connect()` | Initiate connection to the server |
88
- | `waitConnected(signal?)` | Block until the handshake completes |
89
- | `disconnect()` | Gracefully disconnect |
92
+ | Method | Description |
93
+ | --------------------------------------------- | ------------------------------------------------- |
94
+ | `new Client(identity, addr, nickname, opts?)` | Create a new client |
95
+ | `connect({ signal? })` | Connect; resolves once the handshake completes |
96
+ | `disconnect({ signal? })` | Leave the server and close the connection |
97
+ | `status` | `"disconnected"`, `"connecting"` or `"connected"` |
98
+ | `clientID` | Our clid, assigned by the server |
99
+ | `channelID` | The channel we are in |
100
+ | `serverCapabilities` | Server version, family and uid hash |
101
+ | `uid` | Our own uid, hashed as the server does |
102
+
103
+ `connect()` rejects with an `InvalidAddressError` for an address that cannot be
104
+ one — no host, or a port outside 1–65535 — before anything starts; a host name
105
+ that does not resolve fails with a `TeamspeakError` whose `cause` is the lookup's
106
+ error. A `connect()` called while `disconnect()` is still saying goodbye waits
107
+ for it to finish, then connects.
108
+
109
+ Commands, raw `exec`/`execNoWait`, voice and whispers need a connected client:
110
+ until `connect()` resolves, and from the moment `disconnect()` begins, they fail
111
+ with a `NotConnectedError` and send nothing.
90
112
 
91
113
  ### Events
92
114
 
@@ -99,59 +121,204 @@ process.on("SIGINT", async () => {
99
121
  | `on("clientLeave", handler)` | Fires when a client leaves |
100
122
  | `on("clientMoved", handler)` | Fires when a client moves channels |
101
123
  | `on("kicked", handler)` | Fires when the bot is kicked |
102
- | `on("poke", handler)` | Fires when poked by a client |
103
- | `on("voice", handler)` | Fires on incoming voice data |
124
+ | `on("poked", handler)` | Fires when poked by a client |
125
+ | `on("voiceData", handler)` | Fires on incoming voice data |
126
+
127
+ `on()` and `once()` return a function that removes the listener, which is also
128
+ a `Disposable` (`using listener = client.on(...)`). Listeners run one at a
129
+ time, in the order their events arrived, so a listener may call the client and
130
+ await an answer. `disconnected` delivers `undefined` after `disconnect()`, and
131
+ the cause otherwise — a `ConnectionTimeoutError`, or a `RemovedError` for a kick, a ban
132
+ or a server stopping. `await using client = new Client(...)` disconnects when
133
+ the scope ends.
134
+
135
+ `connected` is delivered after `connect()` resolves, so a listener added right
136
+ after `await client.connect()` still hears it. `disconnect()` waits a second at
137
+ most for the server to acknowledge its goodbye, less if its `signal` aborts
138
+ sooner, and resolves once `disconnected` has been delivered. A `connect()` waits
139
+ for an earlier session's `disconnected` to be delivered before it starts, so no
140
+ listener or `events()` iterator of the new session hears the old one end.
141
+
142
+ A listener that throws does not stop the others: the event still reaches the
143
+ listeners after it, and the exception surfaces as an `uncaughtException`, as an
144
+ EventEmitter listener's would.
145
+
146
+ Voice is the exception to "every event in order": while more than 256
147
+ deliveries are waiting, voice frames are dropped (and counted in a warning at
148
+ most every ten seconds) rather than queued without bound.
149
+
150
+ `events(name, { signal })` is the same as an async iterator, buffering events
151
+ that arrive while the loop body runs:
152
+
153
+ ```typescript
154
+ for await (const msg of client.events("textMessage", { signal })) {
155
+ await reply(msg);
156
+ }
157
+ ```
158
+
159
+ `notification` delivers every notification as it arrived, for the ones the SDK
160
+ does not model. `NotificationName` names them:
161
+
162
+ ```typescript
163
+ import { NotificationName } from "@honeybbq/teamspeak-client";
164
+
165
+ client.on("notification", (n) => {
166
+ if (n.name === NotificationName.ChannelEdited) console.log(n.params);
167
+ });
168
+ ```
104
169
 
105
170
  ### Commands
106
171
 
107
- | Function | Description |
108
- | ---------------------------------------------------- | ------------------------------------------ |
109
- | `sendTextMessage(client, targetMode, targetID, msg)` | Send a text message |
110
- | `clientMove(client, clid, channelID, password?)` | Move a client to a channel |
111
- | `poke(client, clid, message)` | Poke a client |
112
- | `client.sendVoice(data, codec)` | Send Opus voice data |
113
- | `client.sendWhisper(data, clientIds, codec)` | Send Opus voice to specific clients |
114
- | `listChannels(client)` | List all channels |
115
- | `listClients(client)` | List all connected clients |
116
- | `getClientInfo(client, clid)` | Get detailed client information |
117
- | `client.execCommand(cmd, timeout?)` | Execute a raw command |
118
- | `client.execCommandWithResponse(cmd, timeout?)` | Execute a command and return response data |
172
+ Every command the voice protocol answers is a typed function, grouped by area.
173
+ Each is addressed by branded ids, so a `ChannelID` cannot be passed where a
174
+ `ClientID` is meant. The constructors — `asChannelID(5)`, `asClientID(3)` and
175
+ the rest — throw a `RangeError` for anything but an integer in the id's wire
176
+ range: 0–65535 for a `ClientID` and the file transfer ids, 0–2^32−1 for a
177
+ `PermissionID` (an `IconID` also takes the signed form), 0–2^64−1 for the
178
+ `bigint` ids. Required arguments are positional; everything optional
179
+ goes in one options object, always the last argument, which also carries
180
+ `signal` and `timeout` to abandon the command or bound how long it waits.
181
+
182
+ ```typescript
183
+ import {
184
+ listChannels,
185
+ createChannel,
186
+ moveClient,
187
+ kickClient,
188
+ sendChannelMessage,
189
+ addServerGroupPermission,
190
+ iterateClientDatabase,
191
+ permissionByName,
192
+ PermissionName,
193
+ asClientID,
194
+ Codec,
195
+ Reason,
196
+ } from "@honeybbq/teamspeak-client";
197
+
198
+ const channels = await listChannels(client, { signal: AbortSignal.timeout(5_000) });
199
+
200
+ const cid = await createChannel(client, {
201
+ name: "Lounge",
202
+ topic: "the quiet one",
203
+ codec: Codec.OpusVoice,
204
+ isPermanent: true,
205
+ });
206
+
207
+ await moveClient(client, client.clientID, cid, { password: "channel-password" });
208
+ await sendChannelMessage(client, "hello");
209
+ await kickClient(client, someone, Reason.KickChannel, { reasonMessage: "take a break" });
210
+
211
+ await addServerGroupPermission(client, sgid, {
212
+ permission: permissionByName(PermissionName.ClientTalkPower),
213
+ value: 50,
214
+ });
215
+
216
+ for await (const entry of iterateClientDatabase(client)) {
217
+ console.log(entry.nickname, entry.lastConnectedAt);
218
+ }
219
+ ```
220
+
221
+ A command that sends content — `createChannel`'s channel, `editChannel`'s
222
+ settings, `updateClient`, `editClient`, `editClientDatabase`,
223
+ `addServerGroupPermission`'s grant, `addBan`'s `BanFields` — takes it as its own
224
+ argument, and `{ signal, timeout }` as a separate one after it: set among the
225
+ content, they are not read.
226
+
227
+ Options objects use camelCase names (`maxClients` for `channel_maxclients`),
228
+ and only the fields you set are sent: an unset field leaves the setting alone,
229
+ while `""` sends it empty — `editChannel(client, cid, { password: "" })` clears
230
+ a password. A key an options type does not name is never sent. The exceptions
231
+ are `editServer` and `editInstance`, which take the raw `virtualserver_*` and
232
+ `serverinstance_*` properties as `getServerInfo` reports them; `PropertyName` names
233
+ the keys.
234
+
235
+ Results are camelCase too. Timestamps are `Date`s (`undefined` where the server
236
+ sends 0 for "never"), durations are milliseconds with an `Ms` suffix
237
+ (`idleTimeMs`, `durationMs`), and a permission is a `Permission`, built with
238
+ `permissionByID` or `permissionByName`. Small enumerations — `GroupType`,
239
+ `Codec`, `PrivilegeKeyType`, `LogLevel` and the rest — are objects of named values with
240
+ a type of the same name.
241
+
242
+ | Module | Covers |
243
+ | ------------ | -------------------------------------------------------------- |
244
+ | `channels` | Create, edit, move, delete, subscribe, channel permissions |
245
+ | `clients` | List, move, kick, poke, edit, the client database, permissions |
246
+ | `server` | Server and instance settings, groups, temp passwords, bindings |
247
+ | `groups` | Channel groups, the permission catalogue, privilege keys |
248
+ | `moderation` | Bans, complaints, offline messages, text messages, the log |
249
+ | `files` | List, delete, rename, create directories, transfer negotiation |
250
+
251
+ Many of these commands are answered over the voice protocol with a
252
+ notification rather than data rows — `servergrouplist` with
253
+ `notifyservergrouplist`, `ftinitupload` with `notifystartupload` after the
254
+ command has already succeeded. The client pairs each answer with its command,
255
+ so this is invisible to callers; [docs/voice-protocol-responses.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/voice-protocol-responses.md)
256
+ has the measured table. A refusal is always thrown, never returned as an empty
257
+ result. A list or find with nothing to report answers `[]`. A command that
258
+ exists to return something — `createChannel`'s id, `initFileUpload`'s key,
259
+ `getClientInfo`'s fields — throws a `NoDataError` when the server answers
260
+ without it, rather than returning an id of 0 or an empty object.
261
+
262
+ For anything outside the typed set, `client.exec(line, { signal?, timeout? })`
263
+ sends a raw command line and resolves to the rows that answer it.
119
264
 
120
265
  ### File Transfers
121
266
 
122
- | Function | Description |
123
- | -------------------------------------- | --------------------------------- |
124
- | `client.fileTransferInitUpload(...)` | Initialize a file upload |
125
- | `client.fileTransferInitDownload(...)` | Initialize a file download |
126
- | `fileTransferDeleteFile(client, ...)` | Delete files on the server |
127
- | `uploadFileData(host, info, reader)` | Transfer file data to the server |
128
- | `downloadFileData(host, info, writer)` | Receive file data from the server |
267
+ Negotiate with the `files` module, then move the bytes over TCP:
268
+
269
+ ```typescript
270
+ import { Readable } from "node:stream";
271
+ import { initFileUpload } from "@honeybbq/teamspeak-client";
272
+
273
+ const info = await initFileUpload(client, cid, "/notes.txt", BigInt(data.length));
274
+ await client.uploadFileData(host, info, Readable.from([data]));
275
+ ```
276
+
277
+ An upload does not replace a file already at the path unless passed
278
+ `{ overwrite: true }`.
279
+
280
+ | Function | Description |
281
+ | ------------------------------------------------------- | --------------------------------- |
282
+ | `initFileUpload(client, ...)` | Negotiate an upload |
283
+ | `initFileDownload(client, ...)` | Negotiate a download |
284
+ | `client.uploadFileData(host, info, data, { signal? })` | Transfer file data to the server |
285
+ | `client.downloadFileData(host, info, out, { signal? })` | Receive file data from the server |
129
286
 
130
287
  ### Identity
131
288
 
132
289
  ```typescript
133
- import { generateIdentity, identityFromString } from "@honeybbq/teamspeak-client";
290
+ import { generateIdentity, parseIdentity } from "@honeybbq/teamspeak-client";
134
291
 
135
292
  // Generate a new identity with security level 8
136
- const identity = generateIdentity(8);
293
+ const identity = await generateIdentity(8);
137
294
 
138
295
  // Export to string for persistent storage
139
- const exported = identity.exportString();
296
+ const exported = identity.export();
140
297
 
141
298
  // Import from a previously exported string
142
- const restored = identityFromString(exported);
299
+ const restored = parseIdentity(exported);
300
+
301
+ // Raise the security level (CPU-intensive). Identities are immutable: this
302
+ // returns a new one with the same key.
303
+ const stronger = await identity.upgradeToLevel(10, { signal });
143
304
 
144
- // Upgrade security level (CPU-intensive)
145
- identity.upgradeToLevel(10);
305
+ // The unique identifier, hashed as a TS3 (sha1) or TS6 (sha256) server does
306
+ const uid = identity.uid("sha1");
146
307
  ```
147
308
 
309
+ The private key never leaves the identity except through `export()`:
310
+ `toString()`, `toJSON()` and `util.inspect()` describe it without the key.
311
+ `parseIdentity` takes exactly what `export()` writes — the key as canonical
312
+ base64 of at most 32 bytes, a colon, and a decimal offset that fits in 64 bits
313
+ — and throws an `InvalidIdentityError` for anything else.
314
+
148
315
  ### Options
149
316
 
150
317
  ```typescript
151
318
  const client = new Client(identity, "ts.example.com", "Bot", {
152
- logger: consoleLogger,
319
+ logger: consoleLogger, // silent unless set
153
320
  resolver: customResolver,
154
- commandMiddleware: [loggingMiddleware],
321
+ commandMiddleware: [timing],
155
322
  eventMiddleware: [filterMiddleware],
156
323
  serverPassword: process.env.TEAMSPEAK_SERVER_PASSWORD,
157
324
  defaultChannel: "Lobby",
@@ -159,48 +326,162 @@ const client = new Client(identity, "ts.example.com", "Bot", {
159
326
  });
160
327
  ```
161
328
 
329
+ Address resolution — the built-in one or a custom `resolver` — has ten seconds
330
+ in all: the `signal` a resolver is given aborts then, and the client connects
331
+ to the address as given.
332
+
333
+ `commandMiddleware` wraps the typed commands: each middleware receives the
334
+ request (its command name and parameters) with its options and returns the
335
+ answering rows, so it can time, retry or answer a command itself. Raw `exec`
336
+ and `execNoWait` lines do not pass through it.
337
+
338
+ ```typescript
339
+ const timing: CommandMiddleware = (next) => async (request, options) => {
340
+ const started = Date.now();
341
+ try {
342
+ return await next(request, options);
343
+ } finally {
344
+ console.log(request.command, Date.now() - started, "ms");
345
+ }
346
+ };
347
+ ```
348
+
162
349
  Connection auth options:
163
350
 
164
351
  - `serverPassword`: plain-text server password; sent as the TeamSpeak protocol hash during `clientinit`
165
352
  - `defaultChannel`: default channel name requested during `clientinit`
166
353
  - `defaultChannelPassword`: plain-text password for `defaultChannel`; sent as the TeamSpeak protocol hash
167
354
 
168
- ## Subpath Exports
355
+ ### Diagnostics
356
+
357
+ The client logs nothing unless given a `logger`. For tracing, it publishes on
358
+ two [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html)
359
+ channels, which cost nothing without a subscriber:
360
+
361
+ - `honeybbq:teamspeak:command` (`commandChannel`), a tracing channel: `start`
362
+ when a command is sent, `asyncEnd` when its answer arrives, `error` when it
363
+ fails.
364
+ - `honeybbq:teamspeak:packet` (`packetChannel`): every command the server sends.
365
+
366
+ Both carry the command's name, never its parameters, which hold password hashes
367
+ and privilege keys.
368
+
369
+ ### Reconnection
370
+
371
+ Off by default: a silent reconnect makes an outage look like a healthy session.
372
+ An explicit `disconnect()` never reconnects, whatever the policy says.
373
+
374
+ ```typescript
375
+ const client = new Client(identity, "ts.example.com", "Bot", {
376
+ reconnect: {
377
+ backoff: { initialDelayMs: 1_000, maxDelayMs: 60_000, factor: 2, jitter: 0.2 },
378
+ // Optional: decide for yourself which failures are worth retrying.
379
+ shouldRetry: (err, attempt) => attempt < 10,
380
+ },
381
+ });
382
+ ```
383
+
384
+ The defaults are conservative because TeamSpeak **bans an IP after roughly
385
+ three rapid connects** — a retry loop without a delay bans itself, and the ban
386
+ outlasts the outage it was recovering from. Recovery is also not instant even
387
+ when it succeeds: the server holds the dropped session for a while and refuses a
388
+ reconnect with `521 too many clones already connected` until it reaps it, so the
389
+ loop keeps trying rather than giving up on the first refusal. It stops once a
390
+ `connect()` of your own has connected the client, and it does not retry an
391
+ address that cannot be one (`InvalidAddressError`).
392
+
393
+ ### Custom transports
169
394
 
170
- The package provides granular subpath exports for advanced use cases:
395
+ `fileTransferSocketFactory` and `socketFactory` are the only seams into the
396
+ network layer. Without them the SDK always creates its own sockets, so an
397
+ application running behind a proxy has no way through. `socketFactory` returns
398
+ a `DatagramSocket` — a `node:dgram` socket, or anything with the few methods
399
+ it names — and `fileTransferSocketFactory` any `Duplex` stream, such as the
400
+ socket a SOCKS library returns.
171
401
 
172
402
  ```typescript
173
- import { Identity } from "@honeybbq/teamspeak-client/crypto";
174
- import { Resolver } from "@honeybbq/teamspeak-client/discovery";
175
- import { PacketHandler } from "@honeybbq/teamspeak-client/transport";
176
- import { buildCommand, parseCommand } from "@honeybbq/teamspeak-client/command";
177
- import { processInit1 } from "@honeybbq/teamspeak-client/handshake";
403
+ const client = new Client(identity, "ts.example.com", "Bot", {
404
+ // UDP, for the voice and command channel.
405
+ socketFactory: (family) => createSocket(family === 6 ? "udp6" : "udp4"),
406
+ // TCP, dialled separately by each file transfer.
407
+ fileTransferSocketFactory: (host, port) => createConnection({ host, port }),
408
+ });
409
+ ```
410
+
411
+ ### Server capabilities
412
+
413
+ ```typescript
414
+ const caps = client.serverCapabilities;
415
+ // {
416
+ // version: "6.0.0-beta13.1 [Build: 1790080330]", major: 6, family: "ts6",
417
+ // uidAlgorithm: "sha256", name: "TeamSpeak 6 Server", iconID: undefined,
418
+ // hostBannerImageURL: undefined, welcomeMessage: "…"
419
+ // }
178
420
  ```
179
421
 
422
+ Read from `initserver` during the handshake, so it costs no extra round trip.
423
+ The uid hash matters because TS3 hashes a client's public key with SHA-1 and TS6
424
+ with SHA-256 — a client that assumes either one computes an identifier the other
425
+ server never sends.
426
+
427
+ ## Compatibility
428
+
429
+ The packages follow semver from 1.0. A few sets are open by nature and may
430
+ gain members in a minor release: the events (`EventMap`, `ServerEventName`),
431
+ `ClientStatus`, `ServerFamily`, `UIDAlgorithm`, `ErrorCode`, `Reason` and the
432
+ enumeration tables. Write a `switch` over any of them with a default case. A
433
+ field the server sends as a number that one of those tables names — a codec, a
434
+ text message's target mode, a group type — is a plain `number`, since a server
435
+ may send a value newer than the table.
436
+
437
+ `DatagramSocket` describes the part of a socket the client uses; should a
438
+ later release use more, the new members will be optional.
439
+
440
+ The [Go SDK](https://github.com/honeybbq/teamspeak-go) makes the same calls
441
+ with the same semantics, apart from what the languages make differ:
442
+
443
+ - Backoff fields: JavaScript takes `undefined` as "the default" and `0` as 0.
444
+ Go cannot tell an unset field from 0, so there a zero field takes the default
445
+ and a negative jitter disables it.
446
+ - A listener that throws: here the exception is rethrown from a microtask, an
447
+ uncaught exception as an EventEmitter listener's would be, and the other
448
+ listeners still get the event. Go recovers a panicking handler, logs it at
449
+ Error, and keeps delivering.
450
+ - Durations are milliseconds with an `Ms` suffix here, `time.Duration` in Go;
451
+ `listClients`' `uid` flag (on by default) is Go's `OmitUID`.
452
+
453
+ ## Packages
454
+
455
+ This package is the voice client. It depends on
456
+ [`@honeybbq/teamspeak-core`](https://github.com/HoneyBBQ/teamspeak-js/tree/main/packages/core) — the command language, ids, errors and
457
+ typed commands it shares with the ServerQuery client — and re-exports all of
458
+ it, so this is the only package to install.
459
+
460
+ The protocol internals (crypto, handshake, transport) are not exported. They
461
+ change with the protocol, and exporting them would have made every change a
462
+ breaking one.
463
+
180
464
  ## Architecture
181
465
 
182
466
  ```
183
- teamspeak-js/
467
+ packages/client/
184
468
  ├── src/
185
469
  │ ├── client.ts # Client lifecycle, connection management
186
- │ ├── api.ts # High-level API (messages, channels, clients)
187
470
  │ ├── commands.ts # Command sending and response tracking
471
+ │ ├── api/voice.ts # Commands only the voice protocol has
472
+ │ ├── capabilities.ts # Server version and uid hash detection
473
+ │ ├── reconnect.ts # Backoff policy and permanent-failure detection
188
474
  │ ├── events.ts # Event handler registration and middleware
189
475
  │ ├── notifications.ts # Server notification parsing and dispatch
190
476
  │ ├── handshake.ts # Protocol handshake orchestration
191
- │ ├── transfer.ts # File transfer operations
477
+ │ ├── transfer.ts # File transfer data connections
192
478
  │ ├── throttle.ts # Token-bucket rate limiter
193
- │ ├── types.ts # Public type definitions
194
- │ ├── errors.ts # Error classes
195
479
  │ ├── crypto/ # ECDH, EAX encryption, identity management
196
480
  │ ├── handshake/ # Crypto handshake and license verification
197
481
  │ ├── transport/ # UDP packet framing, ACK, compression
198
- │ ├── command/ # Command builder and parser
199
482
  │ └── discovery/ # SRV / TSDNS / direct address resolution
200
- ├── examples/
201
- │ └── connect.ts # Minimal connection example
202
- ├── vite.config.ts # Build configuration (Vite library mode)
203
- └── tsconfig.json
483
+ └── examples/
484
+ └── connect.ts # Minimal connection example
204
485
  ```
205
486
 
206
487
  ## Related
@@ -0,0 +1,5 @@
1
+ /** The reason an aborted signal gives, as an error. */
2
+ export declare function abortReason(signal: AbortSignal): unknown;
3
+ /** Settle with `promise`, or reject as soon as `signal` aborts, whichever is first. */
4
+ export declare function abortable<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T>;
5
+ //# sourceMappingURL=abort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../src/abort.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAExD;AAED,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,CAiB7F"}
package/dist/address.d.ts CHANGED
@@ -4,6 +4,14 @@ export interface HostPort {
4
4
  }
5
5
  /** Parse a TeamSpeak address without confusing an IPv6 colon for a port separator. */
6
6
  export declare function splitHostPort(address: string, defaultPort?: string): HostPort;
7
+ /**
8
+ * Parse an address to connect to, and check it can be one: a host, which
9
+ * contains nothing a host name or IP address cannot, and a port from 1 to
10
+ * 65535. Throws an InvalidAddressError otherwise.
11
+ */
12
+ export declare function parseConnectAddress(address: string): HostPort & {
13
+ portNumber: number;
14
+ };
7
15
  export declare function joinHostPort(host: string, port: string): string;
8
16
  export declare function isIpAddress(host: string): boolean;
9
17
  //# sourceMappingURL=address.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,SAAS,GAAG,QAAQ,CA0B7E;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD"}
1
+ {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,SAAS,GAAG,QAAQ,CA0B7E;AAKD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAatF;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Commands only the voice protocol has.
3
+ *
4
+ * ServerQuery has no subscriptions — a query client hears every channel's
5
+ * events once it registers for them — and no `channelgetdescription`, since its
6
+ * `channelinfo` already carries the description. These run on the voice
7
+ * client alone, which is why they take one rather than any CommandExecutor.
8
+ */
9
+ import type { Client } from "../client.js";
10
+ import type { ChannelID, CommandOptions } from "@honeybbq/teamspeak-core";
11
+ /** The channel's description, which `listChannels` omits for bandwidth. */
12
+ export declare function getChannelDescription(client: Client, cid: ChannelID, options?: CommandOptions): Promise<string>;
13
+ /**
14
+ * Subscribe to a channel: receive its clients' events and voice without
15
+ * joining it.
16
+ */
17
+ export declare function subscribeChannel(client: Client, cid: ChannelID, options?: CommandOptions): Promise<void>;
18
+ /** Stop receiving a subscribed channel's events and voice. */
19
+ export declare function unsubscribeChannel(client: Client, cid: ChannelID, options?: CommandOptions): Promise<void>;
20
+ /** Subscribe to every channel on the server. */
21
+ export declare function subscribeAllChannels(client: Client, options?: CommandOptions): Promise<void>;
22
+ /** Unsubscribe from every channel on the server. */
23
+ export declare function unsubscribeAllChannels(client: Client, options?: CommandOptions): Promise<void>;
24
+ //# sourceMappingURL=voice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice.d.ts","sourceRoot":"","sources":["../../src/api/voice.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1E,2EAA2E;AAC3E,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,8DAA8D;AAC9D,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,gDAAgD;AAChD,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,oDAAoD;AACpD,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAEf"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * What the connected server is, and what that means for the caller.
3
+ *
4
+ * TS3 and TS6 differ in ways that stay invisible until something breaks. The
5
+ * clearest: the server hashes a client's public key with SHA-1 on TS3 and
6
+ * SHA-256 on TS6, so a client that assumes either one computes a
7
+ * `client_unique_identifier` the other server never sends.
8
+ *
9
+ * Everything below comes from the handshake — `initserver` carries
10
+ * `virtualserver_version`, the one place the server names itself, along with
11
+ * its name, icon, banner and welcome message — so reading it costs no extra
12
+ * round trip.
13
+ */
14
+ import type { IconID } from "@honeybbq/teamspeak-core";
15
+ import { type UIDAlgorithm } from "./crypto/identity.js";
16
+ /**
17
+ * The server generation a version string belongs to. May gain members in a
18
+ * minor release, as server generations appear: a `switch` over it needs a
19
+ * default case.
20
+ */
21
+ export type ServerFamily = "ts3" | "ts6" | "unknown";
22
+ /** What a server said about itself in the handshake. */
23
+ export interface ServerCapabilities {
24
+ /** The `virtualserver_version` string, exactly as the server sent it. */
25
+ readonly version: string;
26
+ /** Major version number, or 0 when it could not be parsed. */
27
+ readonly major: number;
28
+ /** Which server generation this is. */
29
+ readonly family: ServerFamily;
30
+ /**
31
+ * The hash the server uses for `client_unique_identifier`.
32
+ *
33
+ * Read from the family rather than offered as a setting: the server decides
34
+ * this, and a client that picks differently never matches.
35
+ */
36
+ readonly uidAlgorithm: UIDAlgorithm;
37
+ /** The virtual server's name (`virtualserver_name`). */
38
+ readonly name: string;
39
+ /**
40
+ * The server's icon (`virtualserver_icon_id`), or `undefined` for none. The
41
+ * server sends the icon's CRC32, sometimes as a negative 32-bit number; this
42
+ * is the unsigned value, which names the icon file.
43
+ */
44
+ readonly iconID: IconID | undefined;
45
+ /** The host banner image's URL (`virtualserver_hostbanner_gfx_url`), if one is set. */
46
+ readonly hostBannerImageURL: string | undefined;
47
+ /** The welcome message shown on connecting (`virtualserver_welcomemessage`). */
48
+ readonly welcomeMessage: string;
49
+ }
50
+ /**
51
+ * Read what a server says about itself in `initserver`, or a version string
52
+ * alone, such as "6.0.0-beta13.1 [Build: 1]".
53
+ */
54
+ export declare function parseServerCapabilities(initserver: string | Readonly<Record<string, string>>): ServerCapabilities;
55
+ /**
56
+ * Capabilities of a server that has not been connected to yet.
57
+ *
58
+ * The family is `unknown` — nothing has said which server this is — and
59
+ * `uidAlgorithm` is SHA-1, the TS3 hash.
60
+ */
61
+ export declare function unknownServerCapabilities(): ServerCapabilities;
62
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAEvD,OAAO,EAA0B,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEjF;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,GAAG,SAAS,CAAC;AAErD,wDAAwD;AACxD,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,uFAAuF;IACvF,QAAQ,CAAC,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IAChD,gFAAgF;IAChF,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACpD,kBAAkB,CAqBpB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,IAAI,kBAAkB,CAE9D"}